Question

Je vois des gens demander tout le temps si l'héritage multiple doit ê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 se pendre.

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

Était-ce utile?

La solution

Le problème le plus évident concerne le remplacement de fonction.

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 remplacez pas la doQuelque chose méthode.

Lorsque le compilateur a créé ce code ...

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

... quelle implémentation de la méthode doit-elle utiliser? Sans plus de précisions, il est impossible pour le compilateur de résoudre cette ambiguïté.

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

Des langages comme C ++, Java et C # créent une mise en page basée sur une adresse fixe pour chaque type d'objet. Quelque chose comme ça:

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 à la fois de A et de B , le compilateur doit décider de mettre en forme les données dans AB order ou dans BA order.

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.

Pour gérer ce type de système, vous devez abandonner l'approche de présentation fixe, permettant à chaque objet d'être interrogé pour connaître sa présentation avant de tenter d'appeler les fonctions ou d'accéder à ses champs.

Donc ... histoire longue ... 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 rendra la mise en œuvre du compilateur considérablement plus complexe et qu'ils ne pensent probablement pas que l'avantage en vaut la peine.

Autres conseils

Les problèmes que vous avez mentionnés 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)

E.g. 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 de A et B. Vous devez soit redéfinir foo pour indiquer clairement ce qui sera utilisé si c.foo () est appelé ou sinon vous devez renommer l’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.

Le problème des diamants :

  

une ambiguïté qui se produit 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 dans A, B et C ont surchargé , et D ne l'ignore pas, puis de quelle version de la méthode D hérite-t-elle: celle de B ou celle de C?

     

... C'est ce qu'on appelle le "problème du 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 ...

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.

Supposons que vous avez les objets A et B hérités par C. A et B implémentent tous les deux foo () et C non. J'appelle C.foo (). Quelle implémentation est choisie? Il y a d'autres problèmes, mais ce type de problème est très important.

Le problème principal de l'héritage multiple se résume bien avec l'exemple de tloach. Lors de l'héritage 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 empire 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 constate que lorsque je conçois correctement un objet OO, je n'ai jamais besoin d'un 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 ne convient que pour " is-a " relations.

Il existe d'autres techniques, telles que mixins, qui résolvent les mêmes problèmes sans avoir les mêmes problèmes que l'héritage multiple.

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

De mon point de vue, le pire problème des héritages multiples est la 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 existe ce que j’appelle le "code ***", c’est ce douloureux et répétitif c *** que vous devez écrire dans chacune de vos classes pour obtenir un contexte de données, par exemple.

À mon avis, aucune répétition de code dans une langue moderne ne devrait être absolument nécessaire.

Le système CLOS (Common Lisp Object System) est un autre exemple de quelque chose qui prend en charge MI tout en évitant les problèmes de style C ++: l'héritage reçoit un par défaut raisonnable , tout en vous laissant la liberté de décider explicitement comment appeler exactement le comportement d'un super.

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

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 prendre en charge.

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 pourrait éviter de prendre en charge d’autres fonctionnalités (c’est-à-dire qu’aucune méthode Interface ou Extension n'est nécessaire).

Je pense que soutenir ou pas l'héritage multiple est davantage 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 en Java ...

L’un des objectifs de conception d’infrastructures telles que Java et .NET est de permettre au code compilé de fonctionner avec une version d’une bibliothèque précompilée et de fonctionner aussi bien avec les versions suivantes de cette bibliothèque, même si ces versions ultérieures ajoutent de nouvelles fonctionnalités. Alors que 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, les paradigmes de .NET et Java consistent à distribuer des 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 en fait à 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. / p>

La difficulté avec Java et .NET est que le code est autorisé à hériter des membres et qu’ils y ont un accès implicitement , aux référents membres. Supposons qu’on ait les classes W-Z liées comme ci-dessus:

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 de le faire. construisez 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 () est exécuté, ce que W.Test () ne devrait plus être clair, mais jusqu'à ce que l'utilisateur tente d'exécuter W avec la nouvelle version de Y et Z, aucun élément du système ne pourrait reconnaître l'existence d'un problème (sauf si W était considéré illégitime même avant les modifications apportées à Y et à Z).

Le losange n’est pas un problème, tant que vous n'utilisez pas l'héritage virtuel C ++: dans l'héritage normal, chaque classe de base ressemble à un champ membre (en fait, elles sont présentées dans la RAM vous donner un peu de sucre syntaxique et une possibilité supplémentaire de remplacer davantage de méthodes virtuelles. Cela peut imposer une certaine ambiguïté au moment de la compilation, mais cela 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 désordre). Prenons comme exemple un diagramme en forme de «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 en une seule classe, leurs A sont également fusionnés , période. 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 de savoir qu'il est présent 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.

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 B qui partagent le même A . De cette façon, vous pouvez obtenir un pointeur A * qui pointe sur E , mais vous ne pouvez pas le convertir en pointeur B * bien que L'objet est , en réalité B , car cette conversion est ambiguë. Cette ambiguïté ne peut pas être détectée au moment de la compilation (à moins que le compilateur ne voie l'intégralité 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).

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top