Die beiden Konzepte sind sehr, sehr ähnlich. In normalen OOP-Sprachen fügen wir jedem Objekt eine vtable (oder für Schnittstellen: itable) hinzu:
| this
v
+---+---+---+
| V | a | b | the object with fields a, b
+---+---+---+
|
v
+---+---+---+
| o | p | q | the vtable with method slots o(), p(), q()
+---+---+---+
Dies ermöglicht es uns, ähnliche Methoden wie aufzurufen this->vtable.p(this)
.
In Haskell ähnelt die Methodentabelle eher einem impliziten versteckten Argument:
method :: Class a => a -> a -> Int
würde wie die C ++ - Funktion aussehen
template<typename A>
int method(Class<A>*, A*, A*)
Wo Class<A>
ist eine Instanz der Typklasse Class
für Typ A
. Eine Methode würde wie aufgerufen
typeclass_instance->p(value_ptr);
Die Instanz ist von den Werten getrennt. Die Werte behalten ihren tatsächlichen Typ bei. Während Typklassen einen gewissen Polymorphismus zulassen, ist dies kein Subtyping-Polymorphismus. Das macht es unmöglich, eine Liste von Werten zu erstellen, die a erfüllen Class
. Angenommen, wir haben instance Class Int ...
und instance Class String ...
können keinen heterogenen Listentyp wie [Class]
diesen mit Werten wie erstellen [42, "foo"]
. (Dies ist möglich, wenn Sie die Erweiterung "Existenzielle Typen" verwenden, die effektiv zum Go-Ansatz wechselt.)
In Go implementiert ein Wert keinen festen Satz von Schnittstellen. Folglich kann es keinen vtable-Zeiger haben. Stattdessen werden Zeiger auf Schnittstellentypen als fette Zeiger implementiert , die einen Zeiger auf die Daten und einen weiteren Zeiger auf die itable enthalten:
`this` fat pointer
+---+---+
| | |
+---+---+
____/ \_________
v v
+---+---+---+ +---+---+
| o | p | q | | a | b | the data with
+---+---+---+ +---+---+ fields a, b
itable with method
slots o(), p(), q()
this.itable->p(this.data_ptr)
Die itable wird mit den Daten zu einem fetten Zeiger kombiniert, wenn Sie von einem normalen Wert in einen Schnittstellentyp umwandeln. Sobald Sie einen Schnittstellentyp haben, ist der tatsächliche Datentyp irrelevant geworden. Tatsächlich können Sie nicht direkt auf die Felder zugreifen, ohne Methoden durchzugehen oder die Schnittstelle herunterzuspielen (was möglicherweise fehlschlägt).
Der Ansatz von Go für den Schnittstellenversand ist mit Kosten verbunden: Jeder polymorphe Zeiger ist doppelt so groß wie ein normaler Zeiger. Beim Umwandeln von einer Schnittstelle in eine andere werden die Methodenzeiger in eine neue vtable kopiert. Sobald wir das itable erstellt haben, können wir Methodenaufrufe kostengünstig an viele Schnittstellen senden, unter denen traditionelle OOP-Sprachen leiden. Hier ist m die Anzahl der Methoden in der Zielschnittstelle und b die Anzahl der Basisklassen:
- C ++ schneidet Objekte auf oder muss beim Casting virtuelle Vererbungszeiger verfolgen, hat dann aber einfachen vtable-Zugriff. O (1) oder O (b) Kosten für Upcasting, aber Versand nach O (1) -Methode.
- Die Java Hotspot-VM muss beim Upcasting nichts tun, aber bei der Suche nach Schnittstellenmethoden werden alle von dieser Klasse implementierten itables linear durchsucht. O (1) Upcasting, aber O (b) Methodenversand.
- Python muss beim Upcasting nichts tun, sondern verwendet eine lineare Suche durch eine C3-linearisierte Basisklassenliste. O (1) Upcasting, aber O (b²) Methode Versand? Ich bin mir nicht sicher, wie hoch die algorithmische Komplexität von C3 ist.
- Die .NET-CLR verwendet einen ähnlichen Ansatz wie Hotspot, fügt jedoch eine weitere Indirektionsebene hinzu, um die Speichernutzung zu optimieren. O (1) Upcasting, aber O (b) Methodenversand.
Die typische Komplexität für den Methodenversand ist viel besser, da die Methodensuche häufig zwischengespeichert werden kann, aber die Komplexität im schlimmsten Fall ist ziemlich schrecklich.
Im Vergleich dazu hat Go O (1) oder O (m) Upcasting und O (1) Methodenversand. Haskell hat kein Upcasting (das Einschränken eines Typs mit einer Typklasse ist ein Effekt zur Kompilierungszeit) und den Versand der O (1) -Methode.
[42, "foo"]
. Es ist ein anschauliches Beispiel.