„Powiązanie” odnosi się do rozstrzygnięcia nazwy metody na fragment kodu możliwego do wywołania. Zwykle wywołanie funkcji można rozwiązać w czasie kompilacji lub w czasie łącza. Przykładem języka używającego wiązania statycznego jest C:
int foo(int x);
int main(int, char**) {
printf("%d\n", foo(40));
return 0;
}
int foo(int x) { return x + 2; }
W tym foo(40)
przypadku kompilator może rozwiązać połączenie. To wczesne pozwala na pewne optymalizacje, takie jak wstawianie. Najważniejsze zalety to:
- możemy sprawdzić typ
- możemy optymalizować
Z drugiej strony niektóre języki odraczają rozstrzyganie funkcji do ostatniej możliwej chwili. Przykładem jest Python, w którym możemy na nowo zdefiniować symbole w locie:
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
To jest przykład późnego wiązania. Chociaż sprawia, że rygorystyczne sprawdzanie typów jest nieuzasadnione (sprawdzanie typów może być wykonywane tylko w czasie wykonywania), jest jednak znacznie bardziej elastyczne i pozwala nam wyrażać pojęcia, których nie można wyrazić w ramach statycznego pisania lub wczesnego wiązania. Na przykład możemy dodać nowe funkcje w czasie wykonywania.
Wysyłanie metod, tak jak jest to powszechnie stosowane w „statycznych” językach OOP, znajduje się gdzieś pomiędzy tymi dwoma skrajnościami: Klasa z góry deklaruje rodzaj wszystkich obsługiwanych operacji, więc są one statycznie znane i można je sprawdzić. Następnie możemy zbudować prostą tablicę przeglądową (VTable), która wskazuje na faktyczną implementację. Każdy obiekt zawiera wskaźnik do vtable. System typów gwarantuje, że każdy otrzymany obiekt będzie miał odpowiednią tabelę vtable, ale nie mamy pojęcia w czasie kompilacji, jaka jest wartość tej tabeli odnośników. Dlatego obiektów można używać do przekazywania funkcji jako danych (połowa powodów, dla których OOP i programowanie funkcji są równoważne). Tabele Vtab można łatwo zaimplementować w dowolnym języku obsługującym wskaźniki funkcji, takim jak 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!"
}
Ten rodzaj wyszukiwania metody jest również znany jako „dynamiczna wysyłka” i gdzieś pomiędzy wczesnym wiązaniem a późnym wiązaniem. Uważam dynamiczne wysyłanie metod za centralną właściwość definiującą programowanie OOP, a wszystko inne (np. Enkapsulacja, subtyping,…) jest drugorzędne. Pozwala nam wprowadzić polimorfizm do naszego kodu, a nawet dodać nowe zachowanie do fragmentu kodu bez konieczności jego ponownej kompilacji! W przykładzie C każdy może dodać nowy vtable i przekazać obiekt z tym vtable do sayHelloToMeredith()
.
Chociaż jest to późne wiązanie, nie jest to „ekstremalne późne wiązanie”, za którym opowiada się Kay. Zamiast modelu koncepcyjnego „wysyłanie metod za pomocą wskaźników funkcji” stosuje „wysyłanie metod poprzez przekazywanie komunikatów”. Jest to ważne rozróżnienie, ponieważ przekazywanie wiadomości jest znacznie bardziej ogólne. W tym modelu każdy obiekt ma skrzynkę odbiorczą, w której inne obiekty mogą umieszczać wiadomości. Obiekt odbierający może następnie spróbować zinterpretować tę wiadomość. Najbardziej znanym systemem OOP jest WWW. Tutaj wiadomości są żądaniami HTTP, a serwery są obiektami.
Na przykład mogę zapytać serwer programmers.stackexchange.se GET /questions/301919/
. Porównaj to z notacją programmers.get("/questions/301919/")
. Serwer może odrzucić tę prośbę lub odesłać mi błąd lub może dostarczyć mi twoje pytanie.
Moc przekazywania wiadomości polega na tym, że skaluje się bardzo dobrze: żadne dane nie są udostępniane (tylko przesyłane), wszystko może się odbywać asynchronicznie, a obiekty mogą interpretować wiadomości w dowolny sposób. To sprawia, że przekazujący komunikat system OOP jest łatwo rozszerzalny. Mogę wysyłać wiadomości, które nie wszyscy rozumieją, albo odzyskać oczekiwany wynik lub błąd. Obiekt nie musi deklarować z góry, na które komunikaty odpowie.
To nakłada odpowiedzialność za utrzymanie poprawności na odbiorcę wiadomości, myśl zwaną także enkapsulacją. Np. Nie mogę odczytać pliku z serwera HTTP bez pytania go za pomocą wiadomości HTTP. Dzięki temu serwer HTTP może odrzucić moje żądanie, np. Jeśli nie mam uprawnień. W mniejszej skali OOP oznacza to, że nie mam dostępu do odczytu i zapisu do wewnętrznego stanu obiektu, ale muszę przejść przez metody publiczne. Serwer HTTP też nie musi mi podawać pliku. Może to być dynamicznie generowana zawartość z bazy danych. W rzeczywistym OOP mechanizm reakcji obiektu na komunikaty może zostać wyłączony bez zauważenia przez użytkownika. Jest to silniejsze niż „odbicie”, ale zwykle pełny protokół meta-obiektów. Mój powyższy przykład C nie może zmienić mechanizmu wysyłania w czasie wykonywania.
Możliwość zmiany mechanizmu wysyłania oznacza późne wiązanie, ponieważ wszystkie wiadomości są kierowane za pomocą kodu definiowanego przez użytkownika. Jest to niezwykle potężne: biorąc pod uwagę protokół metaobiektu, mogę dodawać funkcje, takie jak klasy, prototypy, dziedziczenie, klasy abstrakcyjne, interfejsy, cechy, wielokrotne dziedziczenie, wielozadaniowość, programowanie aspektowe, odbicie, zdalne wywoływanie metod, obiekty proxy itp. dla języka, który nie zaczyna się od tych funkcji. Ta moc ewolucji jest całkowicie nieobecna w bardziej statycznych językach, takich jak C #, Java lub C ++.