Ich kann Leute sehen, die ständig fragen, ob Mehrfachvererbung in der nächsten Version von C # oder Java enthalten sein soll. C ++ - Leute, die das Glück haben, diese Fähigkeit zu besitzen, sagen, dass dies so ist, als würde man jemandem ein Seil geben, um sich irgendwann aufzuhängen.
Was ist los mit Mehrfachvererbung? Gibt es konkrete Muster?
oop
multiple-inheritance
language-theory
Vlad Gudim
quelle
quelle
Antworten:
Das offensichtlichste Problem ist das Überschreiben von Funktionen.
Nehmen wir an, Sie haben zwei Klassen
A
undB
beide definieren eine MethodedoSomething
. Jetzt definieren Sie eine dritte KlasseC
, die von beidenA
und erbtB
, aber Sie überschreiben diedoSomething
Methode nicht.Wenn der Compiler diesen Code setzt ...
... welche Implementierung der Methode sollte sie verwenden? Ohne weitere Klarstellung kann der Compiler die Mehrdeutigkeit nicht auflösen.
Neben dem Überschreiben ist das andere große Problem bei der Mehrfachvererbung das Layout der physischen Objekte im Speicher.
Sprachen wie C ++, Java und C # erstellen für jeden Objekttyp ein festes adressbasiertes Layout. Etwas wie das:
Wenn der Compiler Maschinencode (oder Bytecode) generiert, verwendet er diese numerischen Offsets, um auf jede Methode oder jedes Feld zuzugreifen.
Mehrfachvererbung macht es sehr schwierig.
Wenn die Klasse
C
von beidenA
und erbtB
, muss der Compiler entscheiden, ob die Daten in der richtigenAB
Reihenfolge oder in der richtigen ReihenfolgeBA
angeordnet werden sollen.Stellen Sie sich nun vor, Sie rufen Methoden für ein
B
Objekt auf. Ist es wirklich nur einB
? Oder ist es tatsächlich einC
Objekt, das über seineB
Schnittstelle polymorph aufgerufen wird? Abhängig von der tatsächlichen Identität des Objekts ist das physische Layout unterschiedlich und es ist unmöglich, den Versatz der Funktion zu kennen, die an der Anrufstelle aufgerufen werden soll.Die Art und Weise, mit dieser Art von System umzugehen, besteht darin, den Ansatz mit festem Layout aufzugeben, sodass jedes Objekt nach seinem Layout abgefragt werden kann, bevor versucht wird, die Funktionen aufzurufen oder auf seine Felder zuzugreifen.
Also ... lange Rede, kurzer Sinn ... es ist ein Problem für Compilerautoren, die Mehrfachvererbung zu unterstützen. Wenn also jemand wie Guido van Rossum Python entwirft oder wenn Anders Hejlsberg c # entwirft, weiß er, dass die Unterstützung der Mehrfachvererbung die Compiler-Implementierungen erheblich komplexer macht, und vermutlich glauben sie nicht, dass der Nutzen die Kosten wert ist.
quelle
Die Probleme, die ihr erwähnt, sind nicht wirklich schwer zu lösen. Tatsächlich macht Eiffel das perfekt! (und ohne willkürliche Entscheidungen oder was auch immer einzuführen)
Wenn Sie beispielsweise von A und B erben, die beide die Methode foo () haben, möchten Sie natürlich keine willkürliche Auswahl in Ihrer Klasse C, die sowohl von A als auch von B erbt. Sie müssen entweder foo neu definieren, damit klar ist, was sein wird wird verwendet, wenn c.foo () aufgerufen wird oder wenn Sie eine der Methoden in C umbenennen müssen (es könnte bar () werden)
Ich denke auch, dass Mehrfachvererbung oft sehr nützlich ist. Wenn Sie sich die Bibliotheken von Eiffel ansehen, werden Sie feststellen, dass sie überall verwendet werden, und ich persönlich habe die Funktion verpasst, als ich wieder in Java programmieren musste.
quelle
Das Diamantproblem :
quelle
someZ
und ihn zuObject
und dann zu werfen möchteB
? WelchesB
wird es bekommen?Object
Mehrfachvererbung ist eines der Dinge, die nicht oft verwendet werden und missbraucht werden können, aber manchmal benötigt werden.
Ich habe nie verstanden, keine Funktion hinzuzufügen, nur weil sie möglicherweise missbraucht wird, wenn es keine guten Alternativen gibt. Schnittstellen sind keine Alternative zur Mehrfachvererbung. Zum einen können Sie damit keine Vor- oder Nachbedingungen erzwingen. Wie bei jedem anderen Tool müssen Sie wissen, wann und wie es verwendet werden soll.
quelle
assert
?Angenommen, Sie haben Objekte A und B, die beide von C geerbt werden. A und B implementieren beide foo () und C nicht. Ich rufe C.foo () an. Welche Implementierung wird ausgewählt? Es gibt andere Probleme, aber diese Art von Dingen ist groß.
quelle
Das Hauptproblem bei der Mehrfachvererbung lässt sich am Beispiel von tloach gut zusammenfassen. Beim Erben von mehreren Basisklassen, die dieselbe Funktion oder dasselbe Feld implementieren, muss der Compiler eine Entscheidung darüber treffen, welche Implementierung geerbt werden soll.
Dies wird schlimmer, wenn Sie von mehreren Klassen erben, die von derselben Basisklasse erben. (Diamantvererbung, wenn Sie den Vererbungsbaum zeichnen, erhalten Sie eine Diamantform)
Diese Probleme sind für einen Compiler nicht wirklich problematisch. Die Auswahl, die der Compiler hier treffen muss, ist jedoch eher willkürlich, was den Code weitaus weniger intuitiv macht.
Ich finde, dass ich bei einem guten OO-Design niemals Mehrfachvererbung benötige. In Fällen, in denen ich es brauche, stelle ich normalerweise fest, dass ich Vererbung verwendet habe, um Funktionen wiederzuverwenden, während Vererbung nur für "is-a" -Beziehungen geeignet ist.
Es gibt andere Techniken wie Mixins, die dieselben Probleme lösen und nicht die Probleme haben, die bei der Mehrfachvererbung auftreten.
quelle
([..bool..]? "test": 1)
?Ich denke nicht, dass das Diamantproblem ein Problem ist, ich würde diese Sophistik in Betracht ziehen, sonst nichts.
Das aus meiner Sicht schlimmste Problem bei der Mehrfachvererbung sind RAD-Opfer und Menschen, die behaupten, Entwickler zu sein, aber in Wirklichkeit (bestenfalls) nur über halbes Wissen verfügen.
Persönlich würde ich mich sehr freuen, wenn ich endlich etwas in Windows Forms wie diesem tun könnte (es ist kein korrekter Code, aber es sollte Ihnen die Idee geben):
Dies ist das Hauptproblem, das ich habe, wenn ich keine Mehrfachvererbung habe. Sie können etwas Ähnliches mit Schnittstellen tun, aber es gibt das, was ich "s *** code" nenne. Es ist dieses schmerzhafte, sich wiederholende c ***, das Sie in jede Ihrer Klassen schreiben müssen, um beispielsweise einen Datenkontext zu erhalten.
Meiner Meinung nach sollte es absolut keine Notwendigkeit geben, nicht die geringste Wiederholung von Code in einer modernen Sprache.
quelle
Das Common Lisp Object System (CLOS) ist ein weiteres Beispiel für etwas, das MI unterstützt und gleichzeitig Probleme im C ++ - Stil vermeidet: Die Vererbung erhält eine sinnvolle Standardeinstellung , während Sie dennoch die Freiheit haben, explizit zu entscheiden, wie genau das Verhalten eines Supers aufgerufen werden soll .
quelle
An der Mehrfachvererbung selbst ist nichts auszusetzen. Das Problem besteht darin, einer Sprache, die nicht von Anfang an auf Mehrfachvererbung ausgelegt war, Mehrfachvererbung hinzuzufügen.
Die Eiffel-Sprache unterstützt die Mehrfachvererbung ohne Einschränkungen auf sehr effiziente und produktive Weise, aber die Sprache wurde von Anfang an so konzipiert, dass sie diese unterstützt.
Diese Funktion ist für Compilerentwickler komplex zu implementieren, aber es scheint, dass dieser Nachteil durch die Tatsache kompensiert werden könnte, dass eine gute Unterstützung für Mehrfachvererbung die Unterstützung anderer Funktionen vermeiden könnte (dh keine Schnittstelle oder Erweiterungsmethode erforderlich).
Ich denke, dass die Unterstützung der Mehrfachvererbung oder nicht die Frage der Wahl und der Prioritäten ist. Eine komplexere Funktion benötigt mehr Zeit, um korrekt implementiert und betriebsbereit zu sein, und ist möglicherweise kontroverser. Die C ++ - Implementierung kann der Grund sein, warum in C # und Java keine Mehrfachvererbung implementiert wurde ...
quelle
Eines der Entwurfsziele von Frameworks wie Java und .NET besteht darin, zu ermöglichen, dass Code, der kompiliert wird, mit einer Version einer vorkompilierten Bibliothek funktioniert, auch mit nachfolgenden Versionen dieser Bibliothek gleich gut funktioniert, selbst wenn diese nachfolgenden Versionen neue Funktionen hinzufügen. Während das normale Paradigma in Sprachen wie C oder C ++ darin besteht, statisch verknüpfte ausführbare Dateien zu verteilen, die alle benötigten Bibliotheken enthalten, besteht das Paradigma in .NET und Java darin, Anwendungen als Sammlungen von Komponenten zu verteilen, die zur Laufzeit "verknüpft" sind .
Das COM-Modell, das .NET vorausging, versuchte, diesen allgemeinen Ansatz zu verwenden, hatte jedoch keine wirkliche Vererbung. Stattdessen definierte jede Klassendefinition effektiv sowohl eine Klasse als auch eine Schnittstelle mit demselben Namen, die alle öffentlichen Mitglieder enthielt. Instanzen waren vom Klassentyp, während Referenzen vom Schnittstellentyp waren. Das Deklarieren einer Klasse als von einer anderen abgeleitet, entsprach dem Deklarieren einer Klasse als Implementierung der Schnittstelle des anderen und erforderte, dass die neue Klasse alle öffentlichen Mitglieder der Klassen, von denen eine abgeleitet wurde, erneut implementiert. Wenn Y und Z von X und dann W von Y und Z abgeleitet sind, spielt es keine Rolle, ob Y und Z die Mitglieder von X unterschiedlich implementieren, da Z ihre Implementierungen nicht verwenden kann - es muss seine definieren besitzen. W kann Instanzen von Y und / oder Z einkapseln.
Die Schwierigkeit in Java und .NET besteht darin, dass Code Mitglieder erben darf und Zugriff darauf implizit auf die übergeordneten Mitglieder verweist. Angenommen, man hatte Klassen WZ wie oben:
Es scheint, als
W.Test()
sollte das Erstellen einer Instanz von W die Implementierung der inFoo
definierten virtuellen Methode aufrufenX
. Angenommen, Y und Z befanden sich tatsächlich in einem separat kompilierten Modul, und obwohl sie beim Kompilieren von X und W wie oben definiert wurden, wurden sie später geändert und neu kompiliert:Was sollte nun der Effekt des Aufrufs sein, aber bis der Benutzer versuchte, W mit der neuen Version von Y und Z auszuführen, konnte kein Teil des Systems erkennen, dass es ein Problem gab (es sei denn, W wurde sogar als unzulässig angesehen vor den Änderungen an Y und Z).
W.Test()
? Wenn das Programm vor der Verteilung statisch verknüpft werden musste, kann die statische Verknüpfungsphase möglicherweise erkennen, dass das Programm zwar vor dem Ändern von Y und Z keine Mehrdeutigkeit aufwies, die Änderungen an Y und Z jedoch zu Mehrdeutigkeiten geführt haben und der Linker dies ablehnen konnte Erstellen Sie das Programm, es sei denn oder bis eine solche Mehrdeutigkeit behoben ist. Andererseits ist es möglich, dass die Person, die sowohl W als auch die neuen Versionen von Y und Z hat, jemand ist, der das Programm einfach ausführen möchte und keinen Quellcode für irgendetwas davon hat. BeimW.Test()
Laufen wäre nicht mehr klar wasW.Test()
quelle
Der Diamant ist kein Problem, solange man nicht tun so etwas wie C ++ virtuelle Vererbung verwenden: in normaler Vererbung jede Basisklasse ein Mitglied Feld ähnelt (tatsächlich sind sie im RAM auf diese Weise angelegt), Ihnen einige syntaktischen Zucker geben und ein zusätzliche Möglichkeit, mehr virtuelle Methoden zu überschreiben. Dies kann zur Kompilierungszeit zu Unklarheiten führen, ist jedoch normalerweise leicht zu lösen.
Andererseits gerät die virtuelle Vererbung zu leicht außer Kontrolle (und wird dann zu einem Chaos). Betrachten Sie als Beispiel ein "Herz" -Diagramm:
In C ++ ist dies völlig unmöglich: Sobald
F
undG
werden zu einer einzigen Klasse zusammengeführt, werden auch ihreA
s zusammengeführt, Punkt. Das heißt , Sie können nie Basisklassen opak in C ++ betrachten (in diesem Beispiel müssen Sie bauenA
in ,H
so dass Sie wissen müssen , dass es gegenwärtig irgendwo in der Hierarchie). In anderen Sprachen kann es jedoch funktionieren; zum BeispielF
undG
könnte A explizit als „intern“ deklarieren, wodurch eine konsequente Verschmelzung verboten und sie effektiv solide gemacht werden.Ein weiteres interessantes Beispiel ( nicht C ++ - spezifisch):
Hier wird nur
B
die virtuelle Vererbung verwendet. SoE
enthält zweiB
s, die die gleiche teilenA
. Auf diese Weise können Sie einen bekommenA*
Zeiger , dass die Punkte aufE
, aber man kann werfen sie nicht auf einenB*
Zeiger , obwohl das Objekt ist tatsächlichB
als solche Besetzung ist zweideutig, und diese Mehrdeutigkeit kann nicht bei der Kompilierung erkannt werden (es sei denn , der Compiler die sieht ganzes Programm). Hier ist der Testcode:Darüber hinaus kann die Implementierung sehr komplex sein (abhängig von der Sprache; siehe die Antwort von Benjaminism).
quelle