"Bindung" bezieht sich auf das Auflösen eines Methodennamens in einen aufrufbaren Code. Normalerweise kann der Funktionsaufruf zur Kompilierungszeit oder zur Verbindungszeit aufgelöst werden. Ein Beispiel für eine Sprache mit statischer Bindung ist C:
int foo(int x);
int main(int, char**) {
printf("%d\n", foo(40));
return 0;
}
int foo(int x) { return x + 2; }
Hier kann der Aufruf foo(40)
vom Compiler aufgelöst werden. Dies ermöglicht frühzeitig bestimmte Optimierungen wie Inlining. Die wichtigsten Vorteile sind:
- Wir können die Typprüfung durchführen
- Wir können Optimierungen vornehmen
Andererseits verschieben einige Sprachen die Funktionsauflösung auf den letztmöglichen Moment. Ein Beispiel ist Python, wo wir Symbole im laufenden Betrieb neu definieren können:
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
Dies ist ein Beispiel für eine späte Bindung. Es macht zwar eine strenge Typprüfung unangemessen (die Typprüfung kann nur zur Laufzeit durchgeführt werden), ist jedoch weitaus flexibler und ermöglicht es uns, Konzepte auszudrücken, die nicht innerhalb der Grenzen der statischen Typisierung oder der frühen Bindung ausgedrückt werden können. Zum Beispiel können wir zur Laufzeit neue Funktionen hinzufügen.
Der in „statischen“ OOP-Sprachen üblicherweise implementierte Methodenversand liegt irgendwo zwischen diesen beiden Extremen: Eine Klasse deklariert den Typ aller unterstützten Operationen im Voraus, sodass diese statisch bekannt sind und typgeprüft werden können. Wir können dann eine einfache Nachschlagetabelle (VTable) erstellen, die auf die tatsächliche Implementierung verweist. Jedes Objekt enthält einen Zeiger auf eine vtable. Das Typsystem garantiert, dass jedes Objekt, das wir erhalten, eine geeignete vtable hat, aber wir haben zum Zeitpunkt der Kompilierung keine Ahnung, welchen Wert diese Nachschlagetabelle hat. Daher können Objekte verwendet werden, um Funktionen als Daten weiterzugeben (der halbe Grund, warum OOP und Funktionsprogrammierung gleichwertig sind). Vtables können problemlos in jeder Sprache implementiert werden, die Funktionszeiger unterstützt, z. B. 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!"
}
Diese Art der Methodensuche wird auch als "dynamischer Versand" bezeichnet und liegt irgendwo zwischen früher und später Bindung. Ich betrachte den dynamischen Methodenversand als die zentrale definierende Eigenschaft der OOP-Programmierung, wobei alles andere (z. B. Kapselung, Subtypisierung, ...) zweitrangig ist. Es ermöglicht uns, Polymorphismus in unseren Code einzuführen und einem Code sogar neues Verhalten hinzuzufügen, ohne ihn neu kompilieren zu müssen! Im C-Beispiel kann jeder eine neue vtable hinzufügen und ein Objekt mit dieser vtable an übergeben sayHelloToMeredith()
.
Während dies eine späte Bindung ist, ist dies nicht die von Kay favorisierte „extreme späte Bindung“. Anstelle des konzeptionellen Modells „Methodenversand über Funktionszeiger“ verwendet er „Methodenversand über Nachrichtenübermittlung“. Dies ist eine wichtige Unterscheidung, da das Weiterleiten von Nachrichten weitaus allgemeiner ist. In diesem Modell verfügt jedes Objekt über einen Posteingang, in den andere Objekte Nachrichten einfügen können. Das empfangende Objekt kann dann versuchen, diese Nachricht zu interpretieren. Das bekannteste OOP-System ist das WWW. Hier sind Nachrichten HTTP-Anforderungen und Server sind Objekte.
Zum Beispiel kann ich den programmers.stackexchange.se Server fragen GET /questions/301919/
. Vergleichen Sie dies mit der Notation programmers.get("/questions/301919/")
. Der Server kann diese Anfrage ablehnen oder mir einen Fehler zurücksenden oder mir Ihre Frage stellen.
Die Fähigkeit der Nachrichtenübergabe besteht darin, dass sie sehr gut skaliert werden kann: Es werden keine Daten gemeinsam genutzt (nur übertragen), alles kann asynchron erfolgen und Objekte können Nachrichten nach Belieben interpretieren. Dies macht ein OOP-System für die Nachrichtenübermittlung leicht erweiterbar. Ich kann Nachrichten senden, die möglicherweise nicht jeder versteht, und entweder mein erwartetes Ergebnis oder einen Fehler zurückerhalten. Das Objekt muss nicht im Voraus deklarieren, auf welche Nachrichten es antworten wird.
Dies überträgt die Verantwortung für die Aufrechterhaltung der Korrektheit auf den Empfänger einer Nachricht, ein Gedanke, der auch als Kapselung bezeichnet wird. Beispielsweise kann ich eine Datei nicht von einem HTTP-Server lesen, ohne sie über eine HTTP-Nachricht anzufordern. Dadurch kann der HTTP-Server meine Anfrage ablehnen, z. B. wenn mir Berechtigungen fehlen. In kleinerem OOP bedeutet dies, dass ich keinen Lese- / Schreibzugriff auf den internen Status eines Objekts habe, sondern öffentliche Methoden durchlaufen muss. Ein HTTP-Server muss mir auch keine Datei bereitstellen. Es könnte dynamisch generierter Inhalt aus einer Datenbank sein. In der realen OOP kann der Mechanismus, wie ein Objekt auf Nachrichten reagiert, ausgeschaltet werden, ohne dass ein Benutzer dies bemerkt. Dies ist stärker als "Reflexion", aber normalerweise ein vollständiges Metaobjektprotokoll. Mein C-Beispiel oben kann den Versandmechanismus zur Laufzeit nicht ändern.
Die Möglichkeit, den Versandmechanismus zu ändern, impliziert eine späte Bindung, da alle Nachrichten über benutzerdefinierten Code geleitet werden. Und das ist äußerst leistungsfähig: Mit einem Metaobjektprotokoll kann ich Funktionen wie Klassen, Prototypen, Vererbung, abstrakte Klassen, Schnittstellen, Merkmale, Mehrfachvererbung, Mehrfachversand, aspektorientierte Programmierung, Reflexion, Remote-Methodenaufruf, hinzufügen. Proxy-Objekte usw. in eine Sprache, die nicht mit diesen Funktionen beginnt. Diese Fähigkeit zur Weiterentwicklung fehlt in statischeren Sprachen wie C #, Java oder C ++ vollständig.