web-dev-qa-db-fra.com

Quel est le problème exact de l'héritage multiple?

Je peux voir des gens demander tout le temps si l'héritage multiple devrait être inclus dans la prochaine version de C # ou de Java. Les gens de C++, qui ont la chance d'avoir cette capacité, disent que c'est comme donner à quelqu'un une corde pour finalement se pendre.

Quel est le problème avec l'héritage multiple? Y a-t-il des échantillons concrets?

113
Vlad Gudim

Le problème le plus évident concerne les fonctions prioritaires.

Supposons que deux classes A et B définissent toutes deux une méthode doSomething. Vous définissez maintenant une troisième classe C, qui hérite à la fois de A et de B, mais vous ne substituez pas la méthode doSomething.

Lorsque le compilateur introduit ce code ...

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

... quelle implémentation de la méthode devrait-il utiliser? Sans autre clarification, il est impossible pour le compilateur de résoudre l'ambiguïté.

Outre la priorité, l’autre problème avec l’héritage multiple est la disposition des objets physiques en mémoire.

Des langages comme C++ et Java et C # créent une présentation fixe basée sur l'adresse pour chaque type d'objet. Quelque chose comme ceci:

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

Lorsque le compilateur génère un code machine (ou bytecode), il utilise ces décalages numériques pour accéder à chaque méthode ou champ.

L'héritage multiple le rend très délicat.

Si la classe C hérite de A et de B, le compilateur doit décider de mettre en forme les données dans l'ordre de AB ou dans BA ordre.

Mais maintenant, imaginez que vous appelez des méthodes sur un objet B. Est-ce vraiment juste un B? Ou est-ce réellement un objet C appelé de manière polymorphe, via son interface B? En fonction de l'identité réelle de l'objet, la disposition physique sera différente et il est impossible de connaître le décalage de la fonction à appeler sur le site de l'appel.

La façon de gérer ce type de système consiste à abandonner l’approche de présentation fixe, permettant à chaque objet d’être interrogé sur sa présentation avant essayant d’appeler les fonctions ou d’accéder à ses champs.

Donc ... longue histoire ... les auteurs de compilateurs ont bien du mal à supporter l'héritage multiple. Ainsi, lorsque Guido van Rossum conçoit le python ou Anders Hejlsberg le c #, ils savent que la prise en charge de l'héritage multiple compliquera considérablement les implémentations du compilateur et qu'ils ne pensent probablement pas que le bénéfice en vaut la peine.

78
benjismith

Les problèmes que vous mentionnez ne sont pas vraiment difficiles à résoudre. En fait, par exemple Eiffel le fait parfaitement bien! (et sans introduire de choix arbitraires ou quoi que ce soit)

Par exemple. si vous héritez de A et B, les deux ayant la méthode foo (), alors bien sûr, vous ne voulez pas de choix arbitraire dans votre classe C héritant à la fois de A et B. Vous devez soit redéfinir foo pour qu'il soit clair ce qui sera utilisé si c.foo () est appelé ou sinon, vous devez renommer une des méthodes en C. (cela pourrait devenir bar ())

De plus, je pense que l'héritage multiple est souvent très utile. Si vous regardez les bibliothèques d’Eiffel, vous verrez qu’elles sont utilisées partout. Personnellement, j’ai manqué la fonctionnalité lorsque je devais revenir à la programmation en Java.

44
Daniel

Le problème des diamants :

une ambiguïté qui se pose lorsque deux classes B et C héritent de A et que la classe D hérite des deux B et C. S'il existe une méthode en A, B et C ont surchargé , et D ne remplace pas alors quelle version de la méthode D hérite-t-elle: celle de B ou celle de C?

... Ce problème est appelé "problème de diamant" en raison de la forme du diagramme d'héritage de classe dans cette situation. Dans ce cas, la classe A est en haut, B et C séparément en dessous, et D réunit les deux ensemble en bas pour former un losange ...

26
J Francis

L'héritage multiple est l'une de ces choses qui n'est pas souvent utilisée et qui peut être mal utilisée, mais qui est parfois nécessaire.

Je n'ai jamais compris qu'il ne fallait pas ajouter une fonctionnalité, simplement parce qu'elle était mal utilisée, alors qu'il n'y avait pas de bonne alternative. Les interfaces ne sont pas une alternative à l'héritage multiple. D'une part, ils ne vous permettent pas d'appliquer des conditions préalables ou postconditions. Comme tout autre outil, vous devez savoir quand il convient de l’utiliser et comment l’utiliser.

21
KeithB

supposons que vous ayez les objets A et B hérités de C. A et B implémentent foo () et que C ne les utilise pas. J'appelle C.foo (). Quelle implémentation est choisie? Il y a d'autres problèmes, mais ce type de problème est important.

16
tloach

Je ne pense pas que le problème des diamants soit un problème, je considérerais ce sophisme, rien d’autre.

Le pire problème, de mon point de vue, avec l'héritage multiple est RAD) - les victimes et les personnes qui prétendent être des développeurs mais qui en réalité sont coincées avec une demi-connaissance (au mieux).

Personnellement, je serais très heureux si je pouvais enfin faire quelque chose dans Windows Forms comme ceci (ce n'est pas un code correct, mais cela devrait vous donner une idée):

public sealed class CustomerEditView : Form, MVCView<Customer>

C’est le principal problème que j’ai avec l’absence d’héritage multiple. Vous POUVEZ faire quelque chose de similaire avec les interfaces, mais il y a ce que j'appelle "le code ***", c'est ce c *** douloureux et répétitif que vous devez écrire dans chacune de vos classes pour obtenir un contexte de données, par exemple.

À mon avis, il ne devrait absolument pas être nécessaire, ni même infime, de TOUTE répétition de code dans une langue moderne.

5
Turing Complete

Le problème principal de l'héritage multiple se résume bien avec l'exemple de tloach. Lorsqu'il hérite de plusieurs classes de base qui implémentent la même fonction ou le même champ, le compilateur doit décider de l'implémentation à hériter.

Cela devient pire lorsque vous héritez de plusieurs classes qui héritent de la même classe de base. (héritage de diamant, si vous dessinez l'arbre de l'héritage, vous obtenez une forme de diamant)

Ces problèmes ne sont pas vraiment problématiques pour un compilateur. Mais les choix que le compilateur doit faire ici sont plutôt arbitraires, ce qui rend le code beaucoup moins intuitif.

Je trouve que lorsque je fais bien OO conception, je n'ai jamais besoin d'héritage multiple. Dans les cas où j'en ai besoin, je constate généralement que j'utilise l'héritage pour réutiliser des fonctionnalités alors que l'héritage n'est approprié que pour "is- un "relations.

Il existe d'autres techniques telles que mixins qui résolvent les mêmes problèmes et n'ont pas les mêmes problèmes que l'héritage multiple.

5
Mendelt

Le système CLOS (Common LISP Object System) est un autre exemple de solution prenant en charge MI tout en évitant les problèmes de style C++: un héritage reçoit un valeur sensible par défaut , tout en vous laissant la liberté de décider explicitement comment , disons, appelez le comportement d'un super.

3
Frank Shearar

L'un des objectifs de conception de frameworks tels que Java et .NET est de permettre au code compilé de fonctionner avec une version d'une bibliothèque précompilée, de fonctionner aussi bien avec les versions suivantes Même si le paradigme habituel dans des langages tels que C ou C++ consiste à distribuer des exécutables liés statiquement contenant toutes les bibliothèques dont ils ont besoin, le paradigme dans .NET et Java distribue les applications sous forme de collections de composants "liés" au moment de l'exécution.

Le modèle COM qui a précédé .NET a tenté d'utiliser cette approche générale, mais il n'avait pas vraiment d'héritage - chaque définition de classe définissait à la fois une classe et une interface du même nom contenant tous ses membres publics. Les instances étaient du type classe, alors que les références étaient du type interface. Déclarer une classe comme dérivant d'une autre équivaut à déclarer une classe implémentant l'interface de l'autre et obliger la nouvelle classe à réimplémenter tous les membres publics des classes dont elle est dérivée. Si Y et Z dérivent de X et que W dérive de Y et Z, il importera peu que Y et Z implémentent les membres de X différemment, car Z ne pourra pas utiliser leurs implémentations - il devra en définir le type. posséder. W peut encapsuler des instances de Y et/ou Z et enchaîner ses implémentations des méthodes de X, mais il n'y aurait aucune ambiguïté quant à ce que les méthodes de X devraient faire - elles feraient tout ce que le code de Z leur demanderait explicitement de faire.

La difficulté à Java et .NET est que le code est autorisé à hériter des membres et à y avoir accès - implicitement faire référence aux membres parents. Supposons que les classes WZ soient associées à au dessus de:

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();
  }
}

Il semblerait que W.Test() devrait créer une instance de W appeler l'implémentation de la méthode virtuelle Foo définie dans X. Supposons cependant que Y et Z fussent en réalité dans un module compilé séparément et que, bien qu'ils aient été définis comme ci-dessus lors de la compilation de X et W, ils ont ensuite été modifiés et recompilés:

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

Maintenant, quel devrait être l'effet d'appeler W.Test()? Si le programme devait être lié statiquement avant la distribution, l’étape du lien statique pourrait permettre de discerner que, même si le programme ne présentait aucune ambiguïté avant que Y et Z ne soient modifiés, les modifications apportées à Y et Z ont rendu les choses ambiguës et l’éditeur de liens pouvait refuser construire le programme à moins que ou jusqu'à ce que cette ambiguïté soit résolue D'autre part, il est possible que la personne qui possède à la fois W et les nouvelles versions de Y et Z soit simplement quelqu'un qui veuille exécuter le programme et ne dispose d'aucun code source. Lorsque W.Test() s'exécutera, il ne serait plus clair ce que W.Test() devrait faire, mais jusqu'à ce que l'utilisateur tente d'exécuter W avec la nouvelle version de Y et Z, il n'y aurait aucun moyen de le faire du système pouvait reconnaître qu’il y avait un problème (à moins que W ne soit considéré comme illégitime même avant les modifications apportées à Y et à Z).

2
supercat

Il n'y a rien de mal dans l'héritage multiple lui-même. Le problème consiste à ajouter plusieurs héritages à une langue qui n’a pas été conçue dès le départ.

Le langage Eiffel supporte l'héritage multiple sans restrictions de manière très efficace et productive, mais le langage a été conçu à partir de ce moment pour le supporter.

Cette fonctionnalité est complexe à implémenter pour les développeurs de compilateur, mais il semble que cet inconvénient pourrait être compensé par le fait qu’une bonne prise en charge de l’héritage multiple permettrait d’éviter la prise en charge d’autres fonctionnalités (c’est-à-dire qu’il n’est pas nécessaire de recourir à une méthode d’interface ou d’extension).

Je pense que soutenir ou pas l'héritage multiple est plus une question de choix, une question de priorités. Une fonctionnalité plus complexe prend plus de temps pour être correctement implémentée et opérationnelle et peut être plus controversée. L'implémentation C++ peut être la raison pour laquelle l'héritage multiple n'a pas été implémenté en C # et Java ...

2
Christian Lemer

Le losange n’est pas un problème tant que vous ne pas utilisez quelque chose comme l’héritage virtuel C++: dans un héritage normal, chaque classe de base ressemble à un champ membre (en fait, elles sont présentées dans RAM de cette façon), ce qui vous donne un peu de sucre syntaxique et une capacité supplémentaire à remplacer davantage de méthodes virtuelles, ce qui peut imposer une certaine ambiguïté au moment de la compilation, mais il est généralement facile à résoudre.

D'autre part, avec l'héritage virtuel, il devient trop facilement incontrôlable (et devient ensuite un gâchis). Prenons comme exemple un diagramme "cœur":

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

En C++, c'est tout à fait impossible: dès que F et G sont fusionnés dans une seule classe, leurs As sont également fusionnés, point. Cela signifie que vous ne pouvez jamais considérer les classes de base comme opaques en C++ (dans cet exemple, vous devez construire A dans H afin que vous sachiez qu'elle est présente quelque part dans la hiérarchie). Dans d’autres langues, cela peut fonctionner, cependant; Par exemple, F et G pourraient explicitement déclarer A comme "interne", interdisant ainsi toute fusion conséquente et se rendant effectivement solides.

Un autre exemple intéressant (pas spécifique à C++):

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

Ici, seul B utilise l'héritage virtuel. Donc, E contient deux Bs qui partagent le même A. De cette façon, vous pouvez obtenir un A* pointeur qui pointe sur E, mais vous ne pouvez pas le convertir en un fichier B* pointeur bien que l’objet is en fait B en tant que telle est ambigu, et cette ambiguïté ne peut pas être détectée au moment de la compilation (à moins que le compilateur ne voie l’ensemble du programme). Voici le code de test:

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;
}

De plus, la mise en œuvre peut être très complexe (dépend de la langue; voir la réponse de benjismith).

2
number Zero