54 votes

Le langage C# devrait-il avoir un héritage multiple ?

J'ai rencontré de nombreux arguments contre l'inclusion de l'héritage multiple en C#, dont certains (arguments philosophiques mis à part) :

  • L'héritage multiple est trop compliqué et souvent ambigu.
  • Elle n'est pas nécessaire car les interfaces fournissent quelque chose de similaire
  • La composition est un bon substitut lorsque les interfaces ne sont pas appropriées

Je viens du C++ et la puissance et l'élégance de l'héritage multiple me manquent. Bien qu'il ne soit pas adapté à toutes les conceptions logicielles, il est difficile, dans certaines situations, de nier son utilité par rapport aux interfaces, à la composition et à d'autres techniques OO similaires.

L'exclusion de l'héritage multiple signifie-t-elle que les développeurs ne sont pas assez intelligents pour l'utiliser à bon escient et qu'ils sont incapables de résoudre les problèmes complexes lorsqu'ils se présentent ?

Personnellement, j'accueillerais favorablement l'introduction de l'héritage multiple en C# (peut-être en C##).


Addendum : Je serais intéressé de savoir, d'après les réponses, qui vient d'un milieu à héritage unique (ou procédural) et qui vient d'un milieu à héritages multiples. J'ai souvent constaté que les développeurs qui n'ont aucune expérience de l'héritage multiple se contentent souvent de l'argument selon lequel l'héritage multiple n'est pas nécessaire, simplement parce qu'ils n'ont aucune expérience de ce paradigme.

113voto

Marc Gravell Points 482669

Je ne l'ai jamais manqué une seule fois, jamais. Oui, cela [MI] devient compliqué, et oui, les interfaces font un travail similaire à bien des égards - mais ce n'est pas le point le plus important : au sens général, ce n'est tout simplement pas nécessaire la plupart du temps. Même l'héritage unique est surutilisé dans de nombreux cas.

37voto

Chris Cudmore Points 11133

Préférez l'agrégation à l'héritage !

class foo : bar, baz

est souvent mieux gérée avec

class foo : Ibarrable, Ibazzable
{
  ... 
  public Bar TheBar{ set }
  public Baz TheBaz{ set }

  public void BarFunction()
  {
     TheBar.doSomething();
  }
  public Thing BazFunction( object param )
  {
    return TheBaz.doSomethingComplex(param);
  }
}

De cette manière, il est possible d'intervertir différentes implémentations de IBarrable et IBazzable pour créer plusieurs versions de l'application sans avoir à écrire une autre classe.

L'injection de dépendances peut être d'une grande aide à cet égard.

25voto

Mark Cidade Points 53945

L'un des problèmes liés à l'héritage multiple est la distinction entre l'héritage d'interface et l'héritage d'implémentation.

C# dispose déjà d'une implémentation propre de l'héritage d'interface (y compris le choix d'implémentations implicites ou explicites) en utilisant des interfaces pures.

En C++, pour chaque classe que vous spécifiez après les deux points dans la balise class le type d'héritage que vous obtenez est déterminé par le modificateur d'accès ( private , protected ou public ). Avec public vous obtenez tout le désordre de l'héritage multiple - de multiples interfaces sont mélangées avec de multiples implémentations. Avec l'héritage multiple, les interfaces multiples sont mélangées à des implémentations multiples. private l'héritage, vous obtenez simplement la mise en œuvre. Un objet de type " class Foo : private Bar "ne peut jamais être passé à une fonction qui attend un Bar car c'est comme si le Foo ne possède en réalité qu'une classe privée Bar et un champ délégation modèle .

L'héritage d'implémentations multiples pures (qui n'est en fait qu'une délégation automatique) ne pose aucun problème et serait formidable en C#.

En ce qui concerne l'héritage d'interfaces multiples à partir de classes, il existe de nombreuses conceptions possibles pour mettre en œuvre cette fonctionnalité. Chaque langage qui dispose de l'héritage multiple a ses propres règles quant à ce qui se passe lorsqu'une méthode est appelée avec le même nom dans plusieurs classes de base. Certains langages, comme Common Lisp (en particulier le système d'objets CLOS) et Python, disposent d'un protocole de méta-objet dans lequel vous pouvez spécifier la priorité de la classe de base.

Voici une possibilité :

abstract class Gun
{ 
    public void Shoot(object target) {} 
    public void Shoot() {}

    public abstract void Reload();

    public void Cock() { Console.Write("Gun cocked."); }
}

class Camera
{ 
    public void Shoot(object subject) {}

    public virtual void Reload() {}

    public virtual void Focus() {}
}

//this is great for taking pictures of targets!
class PhotoPistol : Gun, Camera
{ 
    public override void Reload() { Console.Write("Gun reloaded."); }

    public override void Camera.Reload() { Console.Write("Camera reloaded."); }

    public override void Focus() {}
}

var    pp      = new PhotoPistol();
Gun    gun     = pp;
Camera camera  = pp;

pp.Shoot();                    //Gun.Shoot()
pp.Reload();                   //writes "Gun reloaded"
camera.Reload();               //writes "Camera reloaded"
pp.Cock();                     //writes "Gun cocked."
camera.Cock();                 //error: Camera.Cock() not found
((PhotoPistol) camera).Cock(); //writes "Gun cocked."
camera.Shoot();                //error:  Camera.Shoot() not found
((PhotoPistol) camera).Shoot();//Gun.Shoot()
pp.Shoot(target);              //Gun.Shoot(target)
camera.Shoot(target);          //Camera.Shoot(target)

Dans ce cas, seule l'implémentation de la première classe listée est implicitement héritée en cas de conflit. La classe des autres types de base doit être explicitement spécifiée pour obtenir leurs implémentations. Pour rendre le système encore plus sûr, le compilateur peut interdire l'héritage implicite en cas de conflit (les méthodes en conflit nécessiteraient toujours un cast).

De plus, vous pouvez mettre en œuvre l'héritage multiple en C# aujourd'hui avec des opérateurs de conversion implicites :

public class PhotoPistol : Gun /* ,Camera */
{
    PhotoPistolCamera camera;

    public PhotoPistol() {
        camera = new PhotoPistolCamera();
    }

    public void Focus() { camera.Focus(); }

    class PhotoPistolCamera : Camera 
    { 
        public override Focus() { }
    }

    public static Camera implicit operator(PhotoPistol p) 
    { 
        return p.camera; 
    }
}

Elle n'est cependant pas parfaite, car elle n'est pas prise en charge par le programme is y as les opérateurs, et System.Type.IsSubClassOf() .

15voto

Lou Franco Points 48823

Voici un cas très utile d'héritage multiple que je rencontre régulièrement.

En tant que fournisseur de boîtes à outils, je ne peux pas modifier les API publiées sous peine de rompre la compatibilité ascendante. Il en résulte que je ne peux jamais ajouter d'éléments à une interface une fois que je l'ai publiée, car cela briserait la compilation pour quiconque l'implémente.

Cela convient aux clients existants, mais les nouveaux verraient cette hiérarchie comme inutilement complexe, et si je la concevais depuis le début, je ne choisirais pas de la mettre en œuvre de cette manière - je dois le faire, sinon je perdrai la compatibilité ascendante. Si l'interface est interne, il me suffit de la compléter et de corriger les implémenteurs.

Dans de nombreux cas, la nouvelle méthode de l'interface a une implémentation par défaut évidente et petite, mais je ne peux pas la fournir.

Je préférerais utiliser des classes abstraites et, lorsque je dois ajouter une méthode, ajouter une méthode virtuelle avec une implémentation par défaut, et c'est ce que nous faisons parfois.

Le problème, bien sûr, est que si cette classe est susceptible d'être mélangée à quelque chose qui étend déjà quelque chose, nous n'avons pas d'autre choix que d'utiliser une interface et de traiter avec des interfaces d'extension.

Si nous pensons avoir ce problème de manière importante, nous optons pour un modèle d'événement riche à la place -- ce qui, je pense, est probablement la bonne réponse en C#, mais tous les problèmes ne sont pas résolus de cette manière -- parfois, vous voulez une interface publique simple, et une interface plus riche pour les extensions.

12voto

David Arno Points 15499

C# prend en charge l'héritage unique, les interfaces et les méthodes d'extension. Ces méthodes fournissent à peu près tout ce que l'héritage multiple fournit, sans les maux de tête qu'il entraîne.

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