125 votes

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

Je vois régulièrement des gens demander si l'héritage multiple devrait être inclus dans la prochaine version de C# ou de Java. Les adeptes du C++, qui ont la chance d'avoir cette capacité, disent que c'est comme donner une corde à quelqu'un pour qu'il finisse par se pendre.

Qu'en est-il de l'héritage multiple ? Existe-t-il des exemples concrets ?

56 votes

Je voudrais juste mentionner que C++ est excellent pour vous donner assez de corde pour vous pendre.

1 votes

Pour une alternative à l'héritage multiple qui aborde (et, IMHO, résout) un grand nombre des mêmes problèmes, regardez les Traits ( iam.unibe.ch/~scg/Recherche/Traits )

53 votes

Je pensais que C++ vous donnait suffisamment de marge de manœuvre pour vous tirer une balle dans le pied.

6voto

Turing Complete Points 535

Je ne pense pas que le problème du diamant soit un problème, je considère que c'est un sophisme, rien d'autre.

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

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 il devrait vous donner l'idée) :

public sealed class CustomerEditView : Form, MVCView<Customer>

C'est le principal problème que je rencontre 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 s***", c'est ce pénible c*** 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 y avoir absolument aucun besoin, pas le moindre, de TOUTE répétition de code dans une langue moderne.

5voto

Mendelt Points 21583

Le principal problème de l'héritage multiple est joliment résumé par l'exemple de Tloach. Lorsque l'on hérite de plusieurs classes de base qui implémentent la même fonction ou le même champ, le compilateur doit prendre une décision sur l'implémentation à hériter.

La situation s'aggrave lorsque vous héritez de plusieurs classes qui héritent de la même classe de base. (héritage en diamant, si vous dessinez l'arbre d'héritage vous obtenez une forme de diamant)

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

J'estime qu'une bonne conception OO ne nécessite jamais d'héritage multiple. Dans les cas où j'en ai besoin, je constate généralement que j'ai utilisé l'héritage pour réutiliser des fonctionnalités, alors que l'héritage n'est approprié que pour les relations de type "is-a".

Il existe d'autres techniques, comme les mixins, qui résolvent les mêmes problèmes et ne posent pas les mêmes problèmes que l'héritage multiple.

3voto

Prajeeth Emanuel Points 101

Il n'y a rien de mal à l'héritage multiple en soi. Le problème est d'ajouter l'héritage multiple à un langage qui n'a pas été conçu avec l'héritage multiple à l'esprit dès le départ.

Le langage Eiffel prend en charge l'héritage multiple sans restrictions de manière très efficace et productive, mais le langage a été conçu dès le départ pour le prendre en charge.

Cette fonctionnalité est complexe à mettre en œuvre pour les développeurs de compilateurs, mais il semble que cet inconvénient puisse être compensé par le fait qu'une bonne prise en charge de l'héritage multiple pourrait éviter la prise en charge d'autres fonctionnalités (par exemple, pas besoin d'interface ou de méthode d'extension).

Je pense que le fait de soutenir ou non l'héritage multiple est davantage une question de choix, une question de priorités. Une fonctionnalité plus complexe prend plus de temps à

3voto

number Zero Points 46

Le diamant n'est pas un problème, à condition que vous ne utiliser quelque chose comme l'héritage virtuel C++ : dans l'héritage normal, chaque classe de base ressemble à un champ membre (en fait, ils sont disposés de cette manière dans RAM), ce qui vous donne un peu de sucre syntaxique et une capacité supplémentaire à surcharger plus de méthodes virtuelles. Cela peut créer une certaine ambiguïté au moment de la compilation, mais c'est généralement facile à résoudre.

En revanche, avec l'héritage virtuel, il est trop facile d'échapper à tout contrôle (et de semer le désordre). Prenons l'exemple d'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 y G sont fusionnés en une seule classe, leur A sont également fusionnés, point final. Cela signifie que vous ne pouvez jamais considérer les classes de base comme opaques en C++ (dans cet exemple, vous devez construire A en H il faut donc savoir qu'il est présent quelque part dans la hiérarchie). Dans d'autres langues, cela peut fonctionner, par exemple, F y G pourraient déclarer explicitement que A est "interne", interdisant ainsi les fusions qui en découlent et se solidifiant de fait.

Un autre exemple intéressant ( no spécifique au C++) :

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

Ici, seuls les B utilise l'héritage virtuel. Ainsi, le E contient deux B qui partagent le même A . De cette façon, vous pouvez obtenir une A* qui pointe vers E mais vous ne pouvez pas le transformer en un B* pointeur bien que l'objet est en fait B en tant que tel est ambigu, et cette ambiguïté ne peut ê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;
}

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

2voto

supercat Points 25534

L'un des objectifs de la conception de cadres tels que Java et .NET est de permettre au code compilé pour fonctionner avec une version d'une bibliothèque précompilée de fonctionner également avec les versions ultérieures de cette bibliothèque, même si ces versions ultérieures ajoutent de nouvelles fonctionnalités. Alors que le paradigme normal dans des langages comme le C ou le C++ est de distribuer des exécutables liés statiquement qui contiennent toutes les bibliothèques dont ils ont besoin, le paradigme dans .NET et Java est de distribuer des applications sous forme de collections de composants qui sont "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'y avait pas vraiment d'héritage - au lieu de cela, chaque définition de classe définissait en fait à la fois une classe et une interface du même nom qui contenait tous ses membres publics. Les instances étaient de type classe, tandis que les références étaient de type interface. Déclarer qu'une classe dérive d'une autre équivaut à déclarer qu'une classe implémente l'interface de l'autre, et exige que la nouvelle classe réimplémente tous les membres publics des classes dont elle dérive. Si Y et Z dérivent de X, puis que W dérive de Y et Z, peu importe que Y et Z implémentent différemment les membres de X, car Z ne pourra pas utiliser leurs implémentations - il devra définir les siennes. W pourrait encapsuler des instances de Y et/ou Z, et enchaîner ses implémentations des méthodes de X à travers les leurs, mais il n'y aurait aucune ambiguïté quant à ce que les méthodes de X devraient faire - elles feraient ce que le code de Z leur ordonnerait explicitement de faire.

La difficulté en Java et en .NET est que le code est autorisé à hériter de membres et à y accéder implicitement se réfèrent aux membres parents. Supposons que les classes W-Z soient liées comme indiqué 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 et appeler l'implémentation de la méthode virtuelle Foo défini dans X . Supposons toutefois que Y et Z se trouvent 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 aient été modifiés ultérieurement et recompilés :

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

Quel est l'effet de l'appel à W.Test() ? Si le programme devait être lié statiquement avant sa distribution, l'étape de liaison statique pourrait être en mesure de discerner que 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 pourrait refuser de compiler le programme tant que cette ambiguïté n'aura pas été 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 quelqu'un qui souhaite simplement exécuter le programme et qui ne possède aucun code source. Lorsque W.Test() les courses, il n'est plus possible de savoir ce qu'est une course. W.Test() devrait le faire, mais tant que l'utilisateur n'a pas essayé d'exécuter W avec la nouvelle version de Y et Z, aucune partie du système ne peut reconnaître qu'il y a un problème (à moins que W n'ait été considéré comme illégitime avant même les changements apportés à Y et Z).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X