Was ist das genaue Problem bei der Mehrfachvererbung?

121

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?

Vlad Gudim
quelle
54
Ich möchte nur erwähnen, dass C ++ großartig ist, um Ihnen genug Seil zu geben, um sich aufzuhängen.
Tloach
1
Eine Alternative zur Mehrfachvererbung, die viele der gleichen Probleme behebt (und IMHO löst), finden Sie unter Merkmale ( iam.unibe.ch/~scg/Research/Traits )
Bevan,
52
Ich dachte, C ++ gibt dir genug Seil, um dich in den Fuß zu schießen.
KeithB
6
Diese Frage scheint anzunehmen, dass es im Allgemeinen ein Problem mit MI gibt, während ich viele Sprachen gefunden habe, in denen MI gelegentlich verwendet wird. Es gibt sicherlich Probleme mit dem Umgang bestimmter Sprachen mit MI, aber mir ist nicht bewusst, dass MI im Allgemeinen erhebliche Probleme hat.
David Thornley

Antworten:

86

Das offensichtlichste Problem ist das Überschreiben von Funktionen.

Nehmen wir an, Sie haben zwei Klassen Aund Bbeide definieren eine Methode doSomething. Jetzt definieren Sie eine dritte Klasse C, die von beiden Aund erbt B, aber Sie überschreiben die doSomethingMethode nicht.

Wenn der Compiler diesen Code setzt ...

C c = new C();
c.doSomething();

... 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:

class A:
    at offset 0 ... "abc" ... 4 byte int field
    at offset 4 ... "xyz" ... 8 byte double field
    at offset 12 ... "speak" ... 4 byte function pointer

class B:
    at offset 0 ... "foo" ... 2 byte short field
    at offset 2 ... 2 bytes of alignment padding
    at offset 4 ... "bar" ... 4 byte array pointer
    at offset 8 ... "baz" ... 4 byte function pointer

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 Cvon beiden Aund erbt B, muss der Compiler entscheiden, ob die Daten in der richtigen ABReihenfolge oder in der richtigen Reihenfolge BAangeordnet werden sollen.

Stellen Sie sich nun vor, Sie rufen Methoden für ein BObjekt auf. Ist es wirklich nur ein B? Oder ist es tatsächlich ein CObjekt, das über seine BSchnittstelle 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.

Benjaminismus
quelle
62
Ehm, Python unterstützt MI
Nemanja Trifunovic
26
Dies sind keine sehr überzeugenden Argumente - das feste Layout ist in den meisten Sprachen überhaupt nicht schwierig. In C ++ ist es schwierig, weil der Speicher nicht undurchsichtig ist und Sie daher möglicherweise Schwierigkeiten mit arithmetischen Zeigerannahmen haben. In Sprachen , in denen Klassendefinitionen statisch sind (wie in Java, C # und C ++) können mehrere Vererbungsnamenskonflikte Kompilierung verboten werden (und C # tut dies ohnehin mit Schnittstellen!).
Eamon Nerbonne
10
Das OP wollte die Probleme nur verstehen, und ich erklärte sie, ohne die Angelegenheit persönlich zu redaktionieren. Ich habe nur gesagt, dass die Sprachdesigner und Compiler-Implementierer "vermutlich nicht glauben, dass der Nutzen die Kosten wert ist".
Benjaminism
12
" Das offensichtlichste Problem ist das Überschreiben von Funktionen. " Dies hat nichts mit dem Überschreiben von Funktionen zu tun. Es ist ein einfaches Mehrdeutigkeitsproblem.
Neugieriger
10
Diese Antwort enthält einige falsche Informationen zu Guido und Python, da Python MI unterstützt. "Ich entschied, dass ich, solange ich die Vererbung unterstützen würde, genauso gut eine einfältige Version der Mehrfachvererbung unterstützen könnte." - Guido van Rossum python-history.blogspot.com/2009/02/… - Außerdem ist die Auflösung von Mehrdeutigkeiten in Compilern ziemlich häufig (Variablen können lokal zu blockieren, lokal zu funktionieren, lokal zu einschließender Funktion, Objektmitglieder, Klassenmitglieder, Globale usw.), ich sehe nicht, wie ein zusätzlicher Bereich einen Unterschied machen würde.
Marcus
46

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
26
Genau. Der Hauptgrund, warum Menschen MI hassen, ist der gleiche wie bei JavaScript oder bei statischer Typisierung: Die meisten Menschen haben nur sehr schlechte Implementierungen davon verwendet - oder haben es sehr schlecht verwendet. Die Beurteilung von MI nach C ++ ist wie die Beurteilung von OOP nach PHP oder die Beurteilung von Automobilen nach Pintos.
Jörg W Mittag
2
@curiousguy: MI führt eine weitere Reihe von Komplikationen ein, über die man sich Sorgen machen muss, genau wie viele der "Funktionen" von C ++. Nur weil es eindeutig ist, ist es nicht einfach, damit zu arbeiten oder zu debuggen. Entfernen Sie diese Kette, da sie vom Thema abweicht und Sie sie trotzdem abgeblasen haben.
Guvante
4
@Guvante Das einzige Problem mit MI in einer Sprache sind beschissene Programmierer, die denken, sie könnten ein Tutorial lesen und plötzlich eine Sprache kennen.
Miles Rout
2
Ich würde argumentieren, dass es bei Sprachfunktionen nicht nur darum geht, die Codierungszeit zu verkürzen. Es geht auch darum, die Ausdruckskraft einer Sprache zu steigern und die Leistung zu steigern.
Miles Rout
4
Außerdem treten Fehler von MI nur auf, wenn Idioten sie falsch verwenden.
Miles Rout
27

Das Diamantproblem :

Eine Mehrdeutigkeit, die entsteht, wenn zwei Klassen B und C von A und Klasse D sowohl von B als auch von C erben. Wenn es in A eine Methode gibt, die B und C überschrieben haben und D sie nicht überschreibt, welche Version von Methode erbt D: die von B oder die von C?

... Es wird wegen der Form des Klassenvererbungsdiagramms in dieser Situation als "Diamantproblem" bezeichnet. In diesem Fall befindet sich Klasse A oben, B und C getrennt darunter, und D verbindet die beiden unten miteinander, um eine Diamantform zu bilden ...

J Francis
quelle
4
Das hat eine Lösung, die als virtuelle Vererbung bekannt ist. Es ist nur ein Problem, wenn Sie es falsch machen.
Ian Goldby
1
@IanGoldby: Virtuelle Vererbung ist ein Mechanismus zur Lösung eines Teils des Problems, wenn nicht identitätserhaltende Upcasts und Downcasts für alle Typen zugelassen werden müssen, von denen eine Instanz abgeleitet ist oder für die sie austauschbar ist . Gegeben X: B; Y: B; und Z: X, Y; Angenommen, someZ ist eine Instanz von Z. Bei der virtuellen Vererbung sind (B) (X) someZ und (B) (Y) someZ unterschiedliche Objekte. Bei beiden kann man den anderen über einen Downcast und einen Upcast bekommen, aber was ist, wenn einer einen hat someZund ihn zu Objectund dann zu werfen möchte B? Welches Bwird es bekommen?
Supercat
2
@supercat Vielleicht, aber solche Probleme sind weitgehend theoretisch und können auf jeden Fall vom Compiler signalisiert werden. Das Wichtigste ist, sich darüber im Klaren zu sein, welches Problem Sie lösen möchten, und dann das beste Werkzeug zu verwenden, um das Dogma von Menschen zu ignorieren, die sich lieber nicht darum kümmern möchten, das Warum zu verstehen.
Ian Goldby
@IanGoldby: Solche Probleme können vom Compiler nur gemeldet werden, wenn er gleichzeitig auf alle fraglichen Klassen zugreifen kann. In einigen Frameworks erfordert jede Änderung an einer Basisklasse immer eine Neukompilierung aller abgeleiteten Klassen. Die Möglichkeit, neuere Versionen von Basisklassen zu verwenden, ohne abgeleitete Klassen neu kompilieren zu müssen (für die möglicherweise kein Quellcode vorhanden ist), ist jedoch eine nützliche Funktion für Frameworks, die es bereitstellen können. Darüber hinaus sind die Probleme nicht nur theoretisch. Viele Klassen in .NET stützen sich auf die Tatsache, dass eine Object
Umwandlung
3
@ IanGoldby: Fair genug. Mein Punkt war, dass die Implementierer von Java und .NET nicht nur "faul" waren, sich dafür zu entscheiden, generalisierte MI nicht zu unterstützen; Die Unterstützung eines generalisierten MI hätte verhindert, dass ihr Rahmen verschiedene Axiome aufrechterhält, deren Gültigkeit für viele Benutzer nützlicher ist als der MI.
Supercat
21

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.

KeithB
quelle
Können Sie erklären, warum Sie damit keine Vor- und Nachbedingungen durchsetzen können?
Yttrill
2
@Yttrill, da Schnittstellen keine Methodenimplementierungen haben können. Wo legst du das hin assert?
Neugieriger
1
@curiousguy: Sie verwenden eine Sprache mit einer geeigneten Syntax, mit der Sie die Vor- und Nachbedingungen direkt in die Benutzeroberfläche einfügen können: Es ist kein "Assert" erforderlich. Beispiel von Felix: fun div (num: int, den: int wenn den! = 0): int Ergebnis erwarten == 0 impliziert num == 0;
Yttrill
@Yttrill OK, aber einige Sprachen, wie Java, unterstützen weder MI noch "Vor- und Nachbedingungen direkt in die Benutzeroberfläche".
neugieriger Kerl
Es wird nicht oft verwendet, da es nicht verfügbar ist und wir nicht wissen, wie man es gut verwendet. Wenn Sie sich einen Scala-Code ansehen, werden Sie sehen, wie die Dinge anfangen, allgemein zu werden und auf Merkmale umgestaltet werden können (Ok, es ist kein MI, aber es beweist meinen Standpunkt).
Santiagobasulto
16

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ß.

tloach
quelle
1
Das ist aber kein konkretes Beispiel. Wenn sowohl A als auch B eine Funktion haben, ist es sehr wahrscheinlich, dass C auch eine eigene Implementierung benötigt. Andernfalls kann es weiterhin A :: foo () in seiner eigenen Funktion foo () aufrufen.
Peter Kühne
@ Quantum: Was ist, wenn es nicht tut? Es ist leicht, das Problem mit einer Vererbungsstufe zu erkennen, aber wenn Sie viele Ebenen haben und eine Zufallsfunktion haben, die irgendwo doppelt so hoch ist, wird dies zu einem sehr schwierigen Problem.
Tloach
Der Punkt ist auch nicht, dass Sie die Methode A oder B nicht aufrufen können, indem Sie angeben, welche Sie möchten. Der Punkt ist, dass es keine gute Möglichkeit gibt, eine auszuwählen, wenn Sie nicht angeben. Ich bin nicht sicher, wie C ++ damit umgeht, aber wenn jemand weiß, könnte das erwähnt werden?
Tloach
2
@tloach - Wenn C die Mehrdeutigkeit nicht auflöst, kann der Compiler diesen Fehler erkennen und einen Fehler zur Kompilierungszeit zurückgeben.
Eamon Nerbonne
@Earmon - Wenn foo () virtuell ist, weiß der Compiler aufgrund des Polymorphismus möglicherweise nicht einmal zur Kompilierungszeit, dass dies ein Problem sein wird.
Tloach
5

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.

Mendelt
quelle
4
Das Kompilierte muss keine willkürliche Auswahl treffen - es kann einfach ein Fehler sein. Was ist in C # die Art von ([..bool..]? "test": 1)?
Eamon Nerbonne
4
In C ++ trifft der Compiler niemals solche willkürlichen Entscheidungen: Es ist ein Fehler, eine Klasse zu definieren, in der der Compiler eine willkürliche Wahl treffen müsste.
neugieriger Kerl
5

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):

public sealed class CustomerEditView : Form, MVCView<Customer>

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.

Turing abgeschlossen
quelle
Ich stimme eher zu, aber ich neige nur dazu: Es besteht die Notwendigkeit einer gewissen Redundanz in jeder Sprache, um Fehler zu erkennen. Auf jeden Fall sollten Sie dem Felix-Entwicklerteam beitreten, da dies ein zentrales Ziel ist. Beispielsweise sind alle Deklarationen gegenseitig rekursiv, und Sie können sowohl vorwärts als auch rückwärts sehen, sodass Sie keine Vorwärtsdeklarationen benötigen (der Gültigkeitsbereich ist wie bei C goto-Labels festgelegt).
Yttrill
Ich stimme dem vollkommen zu - ich bin hier gerade auf ein ähnliches Problem gestoßen . Die Leute reden über das Diamantenproblem, sie zitieren es religiös, aber meiner Meinung nach ist es so leicht zu vermeiden. (Wir müssen unsere Programme nicht alle so schreiben, wie sie die iostream-Bibliothek geschrieben haben.) Die Mehrfachvererbung sollte logisch verwendet werden, wenn Sie ein Objekt haben, das die Funktionalität von zwei unterschiedlichen Basisklassen benötigt, die keine überlappenden Funktionen oder Funktionsnamen haben. In den richtigen Händen ist es ein Werkzeug.
jedd.ahyoung
3
@Turing Complete: Keine Code-Wiederholung: Dies ist eine nette Idee, aber sie ist falsch und unmöglich. Es gibt eine große Anzahl von Verwendungsmustern und wir möchten gemeinsame Muster in die Bibliothek abstrahieren, aber es ist Wahnsinn, sie alle zu abstrahieren, denn selbst wenn wir könnten, ist die semantische Last, sich an alle Namen zu erinnern, zu hoch. Was Sie wollen, ist eine schöne Balance. Vergessen Sie nicht, dass Wiederholung die Dinge strukturiert (Muster impliziert Redundanz).
Yttrill
@mittagessen317: Die Tatsache, dass Code im Allgemeinen nicht so geschrieben werden sollte, dass der 'Diamant' ein Problem darstellt, bedeutet nicht, dass ein Sprach- / Framework-Designer das Problem einfach ignorieren kann. Wenn ein Framework vorsieht, dass Upcasting und Downcasting die Objektidentität beibehalten, möchten Sie, dass spätere Versionen einer Klasse die Anzahl der Typen erhöhen, für die sie ersetzt werden kann, ohne dass dies eine wichtige Änderung darstellt, und die Erstellung von Laufzeit-Typen ermöglichen möchten. Ich glaube nicht, dass dies die Vererbung mehrerer Klassen (im Gegensatz zur Vererbung von Schnittstellen) ermöglichen könnte, während die oben genannten Ziele erreicht werden.
Supercat
3

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 .

Frank Shearar
quelle
Ja, CLOS ist eines der überlegensten Objektsysteme seit den Anfängen des modernen Rechnens in vielleicht sogar längst vergangenen
Zeiten
2

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 ...

Christian Lemer
quelle
1
C ++ Unterstützung für MI ist nicht " sehr effizient und produktiv "?
Neugieriger
1
Eigentlich ist es etwas kaputt in dem Sinne, dass es nicht zu anderen Funktionen von C ++ passt. Die Zuweisung funktioniert bei der Vererbung nicht richtig, geschweige denn bei der Mehrfachvererbung (siehe die wirklich schlechten Regeln). Das korrekte Erstellen von Diamanten ist so schwierig, dass das Normungskomitee die Ausnahmehierarchie durcheinander gebracht hat, um sie einfach und effizient zu halten, anstatt sie korrekt auszuführen. Auf einem älteren Compiler, den ich zu dem Zeitpunkt verwendet habe, als ich dies getestet habe, kosten einige MI-Mixins und Implementierungen grundlegender Ausnahmen über ein Megabyte Code und das Kompilieren dauerte 10 Minuten. Nur die Definitionen.
Yttrill
1
Diamanten sind ein gutes Beispiel. In Eiffel wird der Diamant explizit aufgelöst. Stellen Sie sich zum Beispiel vor, Schüler und Lehrer erben beide von Person. Die Person hat einen Kalender, sodass sowohl Schüler als auch Lehrer diesen Kalender erben. Wenn Sie einen Diamanten erstellen, indem Sie einen TeachingStudent erstellen, der sowohl vom Lehrer als auch vom Schüler erbt, können Sie einen der geerbten Kalender umbenennen, um beide Kalender separat verfügbar zu halten, oder sie zusammenführen, damit er sich eher wie eine Person verhält. Mehrfachvererbung kann gut implementiert werden, erfordert jedoch ein sorgfältiges Design und vorzugsweise von Anfang an ...
Christian Lemer
1
Eiffel-Compiler müssen eine globale Programmanalyse durchführen, um dieses MI-Modell effizient zu implementieren. Für polymorphe Methodenaufrufe werden entweder Dispatcher-Thunks oder spärliche Matrizen verwendet, wie hier erläutert . Dies lässt sich nicht gut mit der separaten Kompilierung von C ++ und der Klassenladefunktion von C # und Java kombinieren.
Cyco130
2

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:

class X { public virtual void Foo() { Console.WriteLine("XFoo"); }
class Y : X {};
class Z : X {};
class W : Y, Z  // Not actually permitted in C#
{
  public static void Test()
  {
    var it = new W();
    it.Foo();
  }
}

Es scheint, als W.Test()sollte das Erstellen einer Instanz von W die Implementierung der in Foodefinierten virtuellen Methode aufrufen X. 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:

class Y : X { public override void Foo() { Console.WriteLine("YFoo"); }
class Z : X { public override void Foo() { Console.WriteLine("ZFoo"); }

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. Beim W.Test()Laufen wäre nicht mehr klar wasW.Test()

Superkatze
quelle
2

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:

  A       A
 / \     / \
B   C   D   E
 \ /     \ /
  F       G
    \   /
      H

In C ++ ist dies völlig unmöglich: Sobald Fund Gwerden zu einer einzigen Klasse zusammengeführt, werden auch ihre As zusammengeführt, Punkt. Das heißt , Sie können nie Basisklassen opak in C ++ betrachten (in diesem Beispiel müssen Sie bauen Ain , Hso dass Sie wissen müssen , dass es gegenwärtig irgendwo in der Hierarchie). In anderen Sprachen kann es jedoch funktionieren; zum Beispiel Fund Gkönnte A explizit als „intern“ deklarieren, wodurch eine konsequente Verschmelzung verboten und sie effektiv solide gemacht werden.

Ein weiteres interessantes Beispiel ( nicht C ++ - spezifisch):

  A
 / \
B   B
|   |
C   D
 \ /
  E

Hier wird nur Bdie virtuelle Vererbung verwendet. So Eenthält zwei Bs, die die gleiche teilen A. Auf diese Weise können Sie einen bekommen A*Zeiger , dass die Punkte auf E, aber man kann werfen sie nicht auf einen B*Zeiger , obwohl das Objekt ist tatsächlich B 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:

struct A { virtual ~A() {} /* so that the class is polymorphic */ };
struct B: virtual A {};
struct C: B {};
struct D: B {};
struct E: C, D {};

int main() {
        E data;
        E *e = &data;
        A *a = dynamic_cast<A *>(e); // works, A is unambiguous
//      B *b = dynamic_cast<B *>(e); // doesn't compile
        B *b = dynamic_cast<B *>(a); // NULL: B is ambiguous
        std::cout << "E: " << e << std::endl;
        std::cout << "A: " << a << std::endl;
        std::cout << "B: " << b << std::endl;
// the next casts work
        std::cout << "A::C::B: " << dynamic_cast<B *>(dynamic_cast<C *>(e)) << std::endl;
        std::cout << "A::D::B: " << dynamic_cast<B *>(dynamic_cast<D *>(e)) << std::endl;
        std::cout << "A=>C=>B: " << dynamic_cast<B *>(dynamic_cast<C *>(a)) << std::endl;
        std::cout << "A=>D=>B: " << dynamic_cast<B *>(dynamic_cast<D *>(a)) << std::endl;
        return 0;
}

Darüber hinaus kann die Implementierung sehr komplex sein (abhängig von der Sprache; siehe die Antwort von Benjaminism).

Nummer Null
quelle
Das ist das eigentliche Problem mit MI. Programmierer benötigen möglicherweise unterschiedliche Auflösungen innerhalb einer Klasse. Eine sprachweite Lösung würde das Mögliche einschränken und Programmierer dazu zwingen, Kludges zu erstellen, damit das Programm ordnungsgemäß funktioniert.
Shawnhcorey