"Прив'язка" відноситься до акта розв'язання назви методу до фрагменту викликаного коду. Зазвичай виклик функції може бути вирішений під час компіляції або під час зв'язку. Прикладом мови, що використовує статичне зв'язування, є C:
int foo(int x);
int main(int, char**) {
printf("%d\n", foo(40));
return 0;
}
int foo(int x) { return x + 2; }
Тут виклик foo(40)
може бути вирішений компілятором. Це рано дозволяє певні оптимізації, такі як вбудована лінія. Найважливіші переваги:
- ми можемо зробити перевірку типу
- ми можемо робити оптимізацію
З іншого боку, деякі мови відкладають роздільну здатність функції на останній можливий момент. Приклад - Python, де ми можемо переглядати символи на льоту:
def foo():
""""call the bar() function. We have no idea what bar is."""
return bar()
def bar():
return 42
print(foo()) # bar() is 42, so this prints "42"
# use reflection to overwrite the "bar" variable
locals()["bar"] = lambda: "Hello World"
print(foo()) # bar() was redefined to "Hello World", so it prints that
bar = 42
print(foo()) # throws TypeError: 'int' object is not callable
Це приклад пізнього зв’язування. Хоча це робить необґрунтовану перевірку типу необґрунтованою (перевірку типу можна проводити лише під час виконання), вона набагато гнучкіша і дозволяє нам висловлювати поняття, які не можуть бути виражені в межах статичного набору тексту або раннього прив’язки. Наприклад, ми можемо додавати нові функції під час виконання.
Диспетчер методів, як зазвичай реалізований у "статичних" мовах OOP, знаходиться десь між цими двома крайнощами: Клас оголошує тип усіх підтримуваних операцій на передній панелі, тому вони є статично відомими і можуть бути перевірені. Потім ми можемо побудувати просту таблицю пошуку (VTable), яка вказує на фактичну реалізацію. Кожен об'єкт містить вказівник на vtable. Система типу гарантує, що будь-який об’єкт, який ми отримаємо, матиме відповідний vtable, але ми не маємо уявлення під час компіляції, яке значення має ця таблиця пошуку. Тому об'єкти можна використовувати для передачі функцій навколо як даних (половина причини, через яку OOP та функціонування функцій є рівнозначними). Vtables можна легко реалізувати будь-якою мовою, яка підтримує функціональні покажчики, наприклад C.
#define METHOD_CALL(object_ptr, name, ...) \
(object_ptr)->vtable->name((object_ptr), __VA_ARGS__)
typedef struct {
void (*sayHello)(const MyObject* this, const char* yourname);
} MyObject_VTable;
typedef struct {
const MyObject_VTable* vtable;
const char* name;
} MyObject;
static void MyObject_sayHello_normal(const MyObject* this, const char* yourname) {
printf("Hello %s, I'm %s!\n", yourname, this->name);
}
static void MyObject_sayHello_alien(const MyObject* this, const char* yourname) {
printf("Greetings, %s, we are the %s!\n", yourname, this->name);
}
static MyObject_VTable MyObject_VTable_normal = {
.sayHello = MyObject_sayHello_normal,
};
static MyObject_VTable MyObject_VTable_alien = {
.sayHello = MyObject_sayHello_alien,
};
static void sayHelloToMeredith(const MyObject* greeter) {
// we have no idea what the VTable contents of my object are.
// However, we do know it has a sayHello method.
// This is dynamic dispatch right here!
METHOD_CALL(greeter, sayHello, "Meredith");
}
int main() {
// two objects with different vtables
MyObject frank = { .vtable = &MyObject_VTable_normal, .name = "Frank" };
MyObject zorg = { .vtable = &MyObject_VTable_alien, .name = "Zorg" };
sayHelloToMeredith(&frank); // prints "Hello Meredith, I'm Frank!"
sayHelloToMeredith(&zorg); // prints "Greetings, Meredith, we are the Zorg!"
}
Цей вид пошуку методів також відомий як "динамічна відправка", і десь між ранньою і пізньою зв'язуваннями. Я вважаю диспетчеризацію динамічних методів центральною визначальною властивістю програмування OOP, причому будь-що інше (наприклад, інкапсуляція, підтипізація ...) є другорядним. Це дозволяє нам ввести поліморфізм у наш код і навіть додати нову поведінку до фрагмента коду без необхідності його перекомпілювати! У прикладі C кожен може додати новий vtable і передати об’єкт із цим vtable sayHelloToMeredith()
.
Незважаючи на те, що це зав'язування пізно, це не є "крайнім пізнім зв'язуванням", яке сприяє Кей. Замість концептуальної моделі "метод відправки через функціональні покажчики" він використовує "метод відправки через передачу повідомлення". Це важлива відмінність, оскільки передача повідомлень набагато загальніша. У цій моделі кожен об’єкт має папку "Вхідні", куди інші об'єкти можуть розміщувати повідомлення. Потім об'єкт, що отримує, може спробувати інтерпретувати це повідомлення. Найвідомішою системою ООП є WWW. Тут повідомлення - це HTTP-запити, а сервери - об’єкти.
Наприклад, я можу запитати сервер programmers.stackexchange.se GET /questions/301919/
. Порівняйте це з позначенням programmers.get("/questions/301919/")
. Сервер може відмовити в цьому запиті або надіслати мені помилку, або він може надіслати мені ваше запитання.
Сила передачі повідомлень полягає в тому, що вона масштабується дуже добре: ніякі дані не обмінюються (передаються тільки), все може відбуватися асинхронно, а об’єкти можуть інтерпретувати повідомлення, як вони завгодно. Це робить повідомлення, що передають систему OOP, легко розширюється. Я можу надсилати повідомлення, які не всі можуть зрозуміти, і або повернути очікуваний результат, або помилку. Об'єкт не повинен попередньо оголошувати, на які повідомлення він відповість.
Це покладає відповідальність за збереження правильності на одержувача повідомлення, думка, також відома як інкапсуляція. Наприклад, я не можу прочитати файл із сервера HTTP, не запитуючи його через повідомлення HTTP. Це дозволяє серверу HTTP відмовитись від мого запиту, наприклад, якщо у мене відсутні дозволи. У меншому масштабі OOP це означає, що я не маю доступу для читання-запису до внутрішнього стану об'єкта, але я повинен пройти публічні методи. HTTP-сервер також не повинен мені подавати файл. Це може бути динамічно генерований контент із БД. У реальному OOP механізм того, як об’єкт реагує на повідомлення, може бути вимкнений, не помічаючи користувача. Це сильніше, ніж "відображення", але, як правило, повний протокол мета-об'єктів. Мій приклад C вище не може змінити механізм відправки під час виконання.
Можливість зміни механізму відправки передбачає пізнє прив'язування, оскільки всі повідомлення перенаправляються через визначений користувачем код. І це надзвичайно потужно: даючи протокол мета-об’єкта, я можу додати такі функції, як класи, прототипи, успадкування, абстрактні класи, інтерфейси, риси, багатонаступне успадкування, мультидиспетчеризація, орієнтоване на аспекти програмування, рефлексія, виклик віддаленого методу, проксі-об'єкти тощо до мови, яка не починається з цих функцій. Ця здатність розвиватися повністю відсутня у більш статичних мовах, таких як C #, Java або C ++.