Frage

Ich kann sehen, wie Leute ständig fragen, ob mehrere Erbschaften in die nächste Version von C# oder Java einbezogen werden sollten. C ++ - Leute, die das Glück haben, diese Fähigkeit zu haben, sagen, dass dies so ist, als würde man jemandem ein Seil geben, um sich irgendwann selbst zu hängen.

Was ist los mit mehreren Vererbung? Gibt es Betonproben?

War es hilfreich?

Lösung

Das offensichtlichste Problem ist das Überschreiben von Funktionen.

Nehmen wir an, wir haben zwei Klassen A und B, beide definieren eine Methode doSomething. Jetzt definieren Sie eine dritte Klasse C, was von beiden erbt A und B, aber du überschreibe das nicht doSomething Methode.

Wenn der Compiler diesen Code säen ...

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

... Welche Implementierung der Methode sollte sie verwenden? Ohne weitere Klarstellung ist es für den Compiler unmöglich, die Mehrdeutigkeit zu lösen.

Das andere große Problem mit mehreren Vererbung ist nicht nur überschreibend, das Layout der physikalischen Objekte im Gedächtnis.

Sprachen wie C ++ und Java und C# erstellen ein festes Adresslayout für jeden Objekttyp. 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 den Maschinencode (oder Bytecode) generiert, verwendet er diese numerischen Offsets, um auf jede Methode oder jedes Feld zuzugreifen.

Multiple Vererbung macht es sehr schwierig.

Wenn Klasse C Erben von beidem A und B, Der Compiler muss entscheiden, ob die Daten in Daten gelegt werden sollen AB Ordnung oder in BA bestellen.

Aber jetzt stellen Sie sich vor, Sie rufen Methoden auf einem an B Objekt. Ist es wirklich nur ein B? Oder ist es tatsächlich a C Objekt wird als polymorph bezeichnet, durch seine B Schnittstelle? Abhängig von der tatsächlichen Identität des Objekts wird das physische Layout unterschiedlich sein, und es ist unmöglich, den Versatz der Funktion zu kennen, um an der Anrufstätte aufzurufen.

Der Weg, um diese Art von System zu behandeln Vor Versuch, die Funktionen aufzurufen oder auf seine Felder zuzugreifen.

Also ... lange Geschichte kurz ... es ist ein Schmerz für Compiler -Autoren im Nacken, mehrere Erbe zu unterstützen. Wenn also jemand wie Guido van Rossum Python entwirft oder wenn Anders Hejlsberg C#entwirft, wissen sie, dass die Unterstützung mehrerer Vererbung die Implementierungen des Compilers erheblich komplexer machen wird, und vermutlich glauben sie nicht, dass der Nutzen die Kosten wert ist.

Andere Tipps

Die Probleme, die ihr erwähnt, sind nicht wirklich so schwer zu lösen. Tatsächlich macht das Eiffel das perfekt! (und ohne willkürliche Entscheidungen oder was auch immer)

ZB, wenn Sie von A und B erben, beide mit Methode foo (), dann möchten Sie natürlich keine willkürliche Wahl in Ihrer Klasse C, die sowohl von A als auch B. Sie erben verwendet, wenn c.foo () aufgerufen wird oder sonst müssen Sie eine der Methoden in C umbenennen (es könnte bar () werden)

Ich denke auch, dass das Multiple Vererbung oft sehr nützlich ist. Wenn Sie sich Bibliotheken von Eiffel ansehen, werden Sie feststellen, dass es überall verwendet wird und ich persönlich die Funktion verpasst habe, als ich wieder in Java programmieren musste.

Das Diamantproblem:

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

... Es wird das "Diamond -Problem" aufgrund der Form des Klassenvererbungsdiagramms in dieser Situation bezeichnet. In diesem Fall befindet sich Klasse A oben, sowohl B als auch C getrennt darunter, und D verbindet sich mit den beiden unten, um eine Diamantform zu bilden ...

Das Multiple Vererbung ist eines der Dinge, die nicht oft verwendet werden und missbraucht werden können, aber manchmal benötigt werden.

Ich habe nie verstanden, dass ich keine Funktion hinzufügte, nur weil es missbraucht wird, wenn es keine guten Alternativen gibt. Schnittstellen sind keine Alternative zu multipler Vererbung. Zum einen können Sie keine Voraussetzungen oder Nachbedingungen durchsetzen. Wie bei jedem anderen Tool müssen Sie wissen, wann es angemessen ist, zu verwenden, und wie Sie es verwenden.

Nehmen wir an, Sie haben Objekte A und B, die beide von C. A und B sowohl Foo () und C geerbt werden. Ich rufe C.Foo () an. Welche Implementierung wird ausgewählt? Es gibt andere Probleme, aber diese Art von Dingen ist ein großer.

Das Hauptproblem bei mehreren Vererbung ist gut mit dem Beispiel von Tloachs zusammengefasst. Wenn Sie von mehreren Basisklassen erben, die dieselbe Funktion oder das gleiche Feld implementieren, muss der Compiler eine Entscheidung darüber treffen, welche Implementierung die Implementierung erben.

Dies wird schlechter, wenn Sie aus mehreren Klassen erben, die aus derselben Basisklasse erben. (Diamond -Vererbung, wenn Sie den Erbebaum zeichnen, erhalten Sie eine Diamantform)

Diese Probleme sind für einen Compiler nicht wirklich problematisch zu überwinden. Aber die Wahl, die der Compiler hier treffen muss, sind eher willkürlich. Dies macht Code weit weniger intuitiv.

Ich finde, dass ich bei einem guten OO -Design nie mehrfacher Erbe brauche. In Fällen brauche ich es normalerweise, dass ich die Vererbung verwendet habe, um die Funktionalität wiederzuverwenden, während die Vererbung nur für "iS-a" -Belationen geeignet ist.

Es gibt andere Techniken wie Mixins, die die gleichen Probleme lösen und nicht die Probleme haben, die die multiple Vererbung hat.

Ich denke nicht, dass das Diamantproblem ein Problem ist, ich würde diese Sophistik in Betracht ziehen, sonst nichts.

Das schlimmste Problem, aus meiner Sicht, mit mehreren Erbschaft sind rad - Opfer und Menschen, die behaupten, Entwickler zu sein, aber in Wirklichkeit halb (bestenfalls) mit halbem Wissen festgehalten.

Persönlich wäre ich sehr glücklich, wenn ich endlich etwas in Windows -Formularen wie diesen 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 mit keinem mehrfacher Erbe habe. Sie können mit Schnittstellen etwas Ähnliches tun, aber es gibt das, was ich "S *** Code" nenne. Es ist diese schmerzhafte Wiederholung, die Sie in jedem Ihrer Klassen schreiben müssen, um zum Beispiel einen Datenkontext zu erhalten.

Meiner Meinung nach sollte es für jede Wiederholung von Code in einer modernen Sprache absolut nicht die geringste Notwendigkeit geben.

Das gemeinsame Lisp-Objektsystem (CLOS) ist ein weiteres Beispiel für etwas, das MI unterstützt und gleichzeitig die Probleme mit C ++-Stilproblemen vermeiden: Vererbung wird a gegeben a Sensible Standard, Während Sie Ihnen dennoch die Freiheit erlauben, explizit zu entscheiden, wie genau das Verhalten eines Supers nennt.

Es ist nichts Falsches an mehreren Vererbung selbst. Das Problem besteht darin, einer Sprache, die von Anfang an nicht mit mehreren Vererbung entworfen wurde, mehrfacher Vererbung hinzuzufügen.

Die Eiffeltelsprache unterstützt die Mehrfachbewegung ohne Einschränkungen auf sehr effiziente und produktive Weise, aber die Sprache wurde aus diesem Start entwickelt, um sie zu unterstützen.

Diese Funktion ist für Compiler -Entwickler komplex zu implementieren. Es scheint jedoch, dass dieser Nachteil durch die Tatsache kompensiert werden könnte, dass eine gute Unterstützung für mehrere Erbschaften die Unterstützung anderer Merkmale vermeiden könnte (dh keine Schnittstelle oder Erweiterungsmethode).

Ich denke, dass es eher eine Frage der Wahl ist, eine Frage der Prioritäten zu unterstützen oder nicht. Eine komplexere Funktion braucht mehr Zeit, um korrekt implementiert und betriebsbereit zu sein, und kann kontroverser sein. Die C ++ - Implementierung kann der Grund sein, warum in C# und Java nicht mehrerer Vererbung implementiert wurde ...

Eines der Designziele von Frameworks wie Java und .NET ist es, Code zu ermöglichen, der zusammengestellt wird, um mit einer Version einer vorbereiteten Bibliothek zu arbeiten, um gleich gut mit nachfolgenden Versionen dieser Bibliothek zu arbeiten, auch wenn diese nachfolgenden Versionen Fügen Sie neue Funktionen hinzu. Während das normale Paradigma in Sprachen wie C oder C ++ darin besteht .

Das COM-Modell, das .NET vorausging, versuchte, diesen allgemeinen Ansatz zu verwenden, aber es gab nicht wirklich Vererbung-stand, und jede Klassendefinition definierte sowohl eine Klasse als auch eine gleichnamige Schnittstelle, die alle ihre öffentlichen Mitglieder enthielt. Instanzen waren vom Klassentyp, während Referenzen vom Schnittstellentyp waren. Deklarierte eine Klasse als aus einem anderen abgeleitete Klasse entsprach einer Klasse als Implementierung der Schnittstelle des anderen und verlangte von der neuen Klasse, alle öffentlichen Mitglieder der Klassen, von denen eine abgeleitet wurde, erneut zu implementieren. Wenn y und z von x abgeben und dann von Y und Z abgeleitet werden, spielt es keine Rolle, ob y und z die Mitglieder von X anders implementieren, da Z ihre Implementierungen nicht nutzen kann-, muss es seine definieren besitzen. W könnte Instanzen von y und/oder z zusammenfassen und seine Implementierungen von Xs Methoden durch sie überkapseln, aber es würde keine Unklarheit darüber geben, welche Methoden X tun sollten-sie würden den Code von Z explizit tun.

Die Schwierigkeit in Java und .NET besteht darin, dass Code Mitglieder erben und auf sie zugreifen kann implizit Beziehen Sie sich auf die Elternmitglieder. Angenommen, man hatte Klassen mit WZ verwandt 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 wie W.Test() Sollte eine Instanz von W erstellen, rufen Sie die Implementierung der virtuellen Methode an Foo definiert in X. Nehmen wir jedoch an, dass y und z tatsächlich in einem separat kompilierten Modul waren, und obwohl sie als oben definiert wurden, wenn x und w zusammengestellt 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 die Auswirkung der Anrufe haben? W.Test()? Wenn das Programm vor der Verteilung statisch verknüpft sein musste, kann die statische Verbindungsstufe möglicherweise erkennen, dass das Programm zwar keine Unklarheit vor y und z hatte, die Änderungen an Y und Z die Dinge mehrdeutig gemacht haben und der Linker es ablehnen könnte Erstellen Sie das Programm, es sei denn oder bis diese Mehrdeutigkeit gelöst ist. Auf der anderen Seite ist es möglich, dass die Person, die sowohl W als auch die neuen Versionen von Y und Z hat, jemand ist, der einfach das Programm ausführen möchte und keinen Quellcode dafür hat. Wann W.Test() Läufe, es wäre nicht mehr klar was W.Test() sollte tun, aber bis der Benutzer versuchte, W mit der neuen Version von y und z auszuführen, würde es keinen Weg geben, einen Teil des Systems zu erkennen, dass es ein Problem gab (es sei denn .

Der Diamant ist kein Problem, solange Sie wie Sie nicht Verwenden Sie so etwas wie eine virtuelle C ++ - virtuelle Vererbung: In normaler Vererbung ähnelt jede Basisklasse einem Mitgliedsfeld (tatsächlich werden sie auf diese Weise in RAM angelegt), was Ihnen einen syntaktischen Zucker und eine zusätzliche Fähigkeit gibt, mehr virtuelle Methoden zu überschreiben. Das kann bei der Kompilierungszeit etwas Unklarheit auferlegen, aber das ist normalerweise leicht zu lösen.

Andererseits geht es mit dem virtuellen Vererbung zu leicht außer Kontrolle (und wird dann zum Chaos). Betrachten Sie als Beispiel ein „Herz“ -Diagramm:

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

In C ++ ist es völlig unmöglich: sobald F und G werden zu einer einzigen Klasse verschmolzen, ihre, ihre AS werden auch zusammengeführt, Zeitraum. Das heißt A in H Sie müssen also wissen, dass es irgendwo in der Hierarchie vorhanden ist). In anderen Sprachen kann es jedoch funktionieren; zum Beispiel, F und G könnte explizit a als „intern“ erklären, so dass es darum geht, die konsequente Verschmelzung zu verbieten und sich effektiv fest zu machen.

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

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

Nur hier B verwendet virtuelle Vererbung. So E enthält zwei Bs, die gleich teilen A. Auf diese Weise können Sie eine bekommen A* Zeiger, auf das hingewiesen wird E, aber du kannst es nicht an einen werfen B* Zeiger obwohl das Objekt ist eigentlich B Als solches ist die Besetzung mehrdeutig, und diese Mehrdeutigkeit kann nicht zum Kompilierungszeit erkannt werden (es sei denn, der Compiler sieht das gesamte 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 Benjismiths Antwort).

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top