Je peux voir des gens demander tout le temps si l'héritage multiple doit être inclus dans la prochaine version de C # ou 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 de béton?
oop
multiple-inheritance
language-theory
Vlad Gudim
la source
la source
Réponses:
Le problème le plus évident est celui du remplacement de fonction.
Disons avoir deux classes
A
etB
, qui définissent toutes deux une méthodedoSomething
. Vous définissez maintenant une troisième classeC
, qui hérite des deuxA
etB
, mais vous ne remplacez pas ladoSomething
méthode.Lorsque le compilateur amorce ce code ...
... quelle implémentation de la méthode doit-elle utiliser? Sans plus de précision, il est impossible pour le compilateur de résoudre l'ambiguïté.
Outre le remplacement, l'autre gros problème avec l'héritage multiple est la disposition des objets physiques en mémoire.
Des langages tels que 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:
Lorsque le compilateur génère du 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 des deuxA
etB
, le compilateur doit décider de mettre en page les données dans l'AB
ordre ou dans l'BA
ordre.Mais maintenant, imaginez que vous appelez des méthodes sur un
B
objet. Est-ce vraiment juste unB
? Ou s'agit-il en fait d'unC
objet appelé polymorphiquement, via sonB
interface? 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 d'appel.La façon de gérer ce type de système est d'abandonner l'approche de mise en page fixe, permettant à chaque objet d'être interrogé pour sa mise en page avant d' essayer d'appeler les fonctions ou d'accéder à ses champs.
Donc ... longue histoire courte ... c'est une douleur dans le cou pour les auteurs de compilateurs de prendre en charge l'héritage multiple. Ainsi, quand quelqu'un comme Guido van Rossum conçoit python, ou quand Anders Hejlsberg conçoit c #, ils savent que la prise en charge de l'héritage multiple va rendre les implémentations du compilateur beaucoup plus complexes, et vraisemblablement, ils ne pensent pas que l'avantage en vaut le coût.
la source
Les problèmes que vous mentionnez ne sont pas vraiment difficiles à résoudre. En fait, par exemple, Eiffel le fait parfaitement! (et sans introduire de choix arbitraires ou autre)
Par exemple, si vous héritez de A et B, tous deux ayant la méthode foo (), alors bien sûr, vous ne voulez pas d'un choix arbitraire dans votre classe C héritant à la fois de A et B. Vous devez soit redéfinir foo pour que ce soit clair. utilisé si c.foo () est appelé ou sinon vous devez renommer l'une des méthodes en C. (cela pourrait devenir bar ())
Je pense également que l'héritage multiple est souvent très utile. Si vous regardez les bibliothèques d'Eiffel, vous verrez qu'elle est utilisée partout et personnellement, j'ai manqué la fonctionnalité lorsque j'ai dû revenir à la programmation en Java.
la source
Le problème du diamant :
la source
someZ
et veut le lancer versObject
et ensuite versB
? LequelB
obtiendra-t-il?Object
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 de ne pas ajouter de fonctionnalité, simplement parce qu'elle pourrait être mal utilisée, alors qu'il n'y avait pas de bonnes alternatives. 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 des post-conditions. Tout comme tout autre outil, vous devez savoir quand il est approprié de l'utiliser et comment l'utiliser.
la source
assert
?disons que vous avez des objets A et B qui sont tous deux hérités par C. A et B implémentent tous les deux foo () et C ne le fait 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.
la source
Le principal problème de l'héritage multiple est bien résumé 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 prendre une décision sur l'implémentation à hériter.
Cela s'aggrave lorsque vous héritez de plusieurs classes qui héritent de la même classe de base. (héritage du diamant, si vous dessinez l'arbre d'héritage, vous obtenez une forme de diamant)
Ces problèmes ne sont pas vraiment problématiques pour un compilateur à surmonter. 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 une bonne conception OO, je n'ai jamais besoin d'héritage multiple. Dans les cas où j'en ai besoin, je trouve 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 les relations "is-a".
Il existe d'autres techniques comme les mixins qui résolvent les mêmes problèmes et n'ont pas les problèmes de l'héritage multiple.
la source
([..bool..]? "test": 1)
?Je ne pense pas que le problème du diamant soit un problème, je considérerais que c'est du sophisme, rien d'autre.
Le pire problème, de mon point de vue, avec l'héritage multiple est RAD - les victimes et les gens qui prétendent être des développeurs mais en réalité sont coincés avec une demi-connaissance (au mieux).
Personnellement, je serais très heureux si je pouvais enfin faire quelque chose dans Windows Forms comme celui-ci (ce n'est pas du code correct, mais cela devrait vous donner l'idée):
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 "s *** code", c'est ce douloureux 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, pour AUCUNE répétition de code dans un langage moderne.
la source
Le Common Lisp Object System (CLOS) 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 une valeur par défaut raisonnable , tout en vous laissant la liberté de décider explicitement comment, par exemple, appeler le comportement d'un super .
la source
Il n'y a rien de mal dans l'héritage multiple lui-même. Le problème est d'ajouter l'héritage multiple à un langage qui n'a pas été conçu avec l'héritage multiple 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 à partir de là pour le supporter.
Cette fonctionnalité est complexe à implémenter pour les développeurs de compilateurs, mais il semble que cet inconvénient pourrait être compensé par le fait qu'un bon support d'héritage multiple pourrait éviter le support d'autres fonctionnalités (c'est-à-dire pas besoin d'interface ou de méthode d'extension).
Je pense que soutenir ou non 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 ...
la source
L'un des objectifs de conception de frameworks tels que Java et .NET est de permettre au code qui est compilé de fonctionner avec une version d'une bibliothèque précompilée, de fonctionner aussi bien avec les versions ultérieures de cette bibliothèque, même si ces versions ultérieures ajouter de nouvelles fonctionnalités. Alors que le paradigme normal dans des langages comme C ou C ++ est de distribuer des exécutables liés statiquement qui contiennent toutes les bibliothèques dont ils ont besoin, le paradigme en .NET et Java est de distribuer les 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'avait pas vraiment d'héritage - au lieu de cela, chaque définition de classe définissait effectivement à 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 une classe comme dérivant d'une autre équivaut à déclarer une classe comme implémentant l'interface de l'autre, et oblige la nouvelle classe à réimplémenter tous les membres publics des classes dont l'un est dérivé. Si Y et Z dérivent de X, et alors W dérive de Y et Z, peu importe si Y et Z implémentent les membres de X différemment, car Z ne pourra pas utiliser leurs implémentations - il devra définir son posséder. W peut encapsuler des instances de Y et / ou Z,
La difficulté dans Java et .NET est que le code est autorisé à hériter des membres et y avoir accès se réfère implicitement aux membres parents. Supposons que l'on ait des classes WZ liées comme ci-dessus:
Il semblerait que la
W.Test()
création d'une instance de W appelle l'implémentation de la méthode virtuelleFoo
définie dansX
. Supposons, cependant, que Y et Z étaient en fait dans un module compilé séparément, et bien qu'ils aient été définis comme ci-dessus lorsque X et W ont été compilés, ils ont ensuite été modifiés et recompilés:Maintenant, quel devrait être l'effet de l'appel
W.Test()
? Si le programme devait être lié statiquement avant la distribution, l'étape de liaison statique pourrait être en mesure de discerner que si le programme n'avait 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 construire le programme à moins ou jusqu'à ce que cette ambiguïté soit résolue. D'un autre côté, 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 n'a aucun code source pour rien. Lorsqu'ilW.Test()
s'exécute, il ne serait plus clairW.Test()
devrait 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 pourrait reconnaître qu'il y avait un problème (à moins que W ne soit considéré comme illégitime avant même les changements de Y et Z) .la source
Le diamant n'est pas un problème, tant que vous n'utilisez rien 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 dans la RAM de cette façon), vous donnant un peu de sucre syntaxique et un possibilité supplémentaire de remplacer davantage de méthodes virtuelles. Cela peut imposer une certaine ambiguïté au moment de la compilation, mais c'est généralement facile à résoudre.
D'un autre côté, avec l'héritage virtuel, il devient trop facilement incontrôlable (et devient alors un désordre). Prenons comme exemple un diagramme «cœur»:
En C ++, c'est totalement impossible: dès que
F
etG
sont fusionnés en une seule classe, leursA
s sont fusionnés aussi, point final. Cela signifie que vous ne pouvez jamais considérer les classes de base opaque en C ++ (dans cet exemple vous devez construireA
enH
sorte que vous devez savoir qu'il présente quelque part dans la hiérarchie). Dans d'autres langues, cela peut cependant fonctionner; par exemple,F
etG
pourrait explicitement déclarer A comme «interne», interdisant ainsi la fusion conséquente et se rendant effectivement solides.Un autre exemple intéressant ( pas spécifique à C ++):
Ici,
B
n'utilise que l' héritage virtuel.E
Contient donc deuxB
s qui partagent le mêmeA
. De cette façon, vous pouvez obtenir unA*
pointeur qui pointe versE
, mais vous ne pouvez pas le convertir enB*
pointeur bien que l'objet soit en fait enB
tant que tel, le cast est ambigu, et cette ambiguïté ne peut pas être détectée au moment de la compilation (à moins que le compilateur ne voie le programme entier). Voici le code de test:De plus, l'implémentation peut être très complexe (dépend de la langue; voir la réponse de benjismith).
la source