1662 votes

Pourquoi ne pas hériter de List<T> ?

Lorsque je planifie mes programmes, je commence souvent par une chaîne de pensée comme celle-ci :

Une équipe de football est juste une liste de joueurs de football. Par conséquent, je devrais la représenter par :

var football_team = new List<FootballPlayer>();

L'ordre de cette liste représente l'ordre dans lequel les joueurs sont listés dans le roster.

Mais je me rends compte par la suite que les équipes ont également d'autres propriétés, outre la simple liste des joueurs, qui doivent être enregistrées. Par exemple, le total des points marqués cette saison, le budget actuel, les couleurs de l'uniforme, etc. string représentant le nom de l'équipe, etc.

Alors, je pense :

D'accord, une équipe de football est comme une liste de joueurs, mais en plus, elle a un nom (une string ) et un total courant des scores (un int ). .NET ne fournit pas de classe pour le stockage des équipes de football, je vais donc créer ma propre classe. La structure existante la plus similaire et la plus pertinente est List<FootballPlayer> et j'en hériterai donc :

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

Mais il s'avère que une directive dit que vous ne devriez pas hériter de List<T> . Cette ligne directrice me laisse profondément perplexe à deux égards.

Pourquoi pas ?

Apparemment List est en quelque sorte optimisé pour les performances . Comment ? Quels problèmes de performance vais-je causer si je prolonge List ? Qu'est-ce qui va se casser exactement ?

Une autre raison que j'ai vue est que List est fournie par Microsoft, et je n'ai aucun contrôle sur elle, donc Je ne peux pas le modifier plus tard, après avoir exposé une "API publique". . Mais j'ai du mal à comprendre. Qu'est-ce qu'une API publique et pourquoi devrais-je m'en soucier ? Si mon projet actuel n'a pas et n'aura probablement jamais cette API publique, puis-je ignorer cette directive sans risque ? Si j'hérite de List y s'il s'avère que j'ai besoin d'une API publique, quelles difficultés vais-je rencontrer ?

Pourquoi est-ce important ? Une liste est une liste. Qu'est-ce qui pourrait changer ? Qu'est-ce que je pourrais bien vouloir changer ?

Et enfin, si Microsoft ne voulait pas que j'hérite de List pourquoi n'ont-ils pas fait la classe sealed ?

Qu'est-ce que je suis censé utiliser d'autre ?

Apparemment, pour les collections personnalisées, Microsoft a prévu une Collection qui devrait être étendue à la place de la classe List . Mais cette classe est très dépouillée, et ne comporte pas beaucoup de choses utiles, comme AddRange par exemple. La réponse de jvitor83 fournit une justification de performance pour cette méthode particulière, mais comment une méthode lente peut-elle être utilisée ? AddRange pas mieux que non AddRange ?

Héritant de Collection représente beaucoup plus de travail que d'hériter de List et je n'y vois aucun avantage. Microsoft ne me demanderait certainement pas de faire du travail supplémentaire sans raison, mais je ne peux m'empêcher de penser que j'ai mal compris quelque chose et que j'ai hérité d'un héritage. Collection n'est en fait pas la bonne solution pour mon problème.

J'ai vu des suggestions telles que la mise en œuvre IList . Mais non. Ce sont des dizaines de lignes de code passe-partout qui ne m'apportent rien.

Enfin, certains suggèrent d'envelopper le List dans quelque chose :

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Il y a deux problèmes avec cela :

  1. Cela rend mon code inutilement verbeux. Je dois maintenant appeler my_team.Players.Count au lieu de seulement my_team.Count . Heureusement, avec C#, je peux définir des indexeurs pour rendre l'indexation transparente, et transmettre toutes les méthodes de l'indexation interne à l'entreprise. List ... Mais c'est beaucoup de code ! Qu'est-ce que je gagne pour tout ce travail ?

  2. Ça n'a tout simplement aucun sens. Une équipe de football ne "possède" pas une liste de joueurs. Elle est la liste des joueurs. On ne dit pas "John McFootballer a rejoint les joueurs de Uneéquipe". On dit "Jean a rejoint Une certaine équipe". On n'ajoute pas une lettre aux "caractères d'une chaîne", on ajoute une lettre à une chaîne. Vous n'ajoutez pas un livre aux livres d'une bibliothèque, vous ajoutez un livre à une bibliothèque.

Je réalise que ce qui se passe "sous le capot" peut être considéré comme "l'ajout de X à la liste interne de Y", mais cela semble être une façon très contre-intuitive de penser le monde.

Ma question (résumée)

Quelle est la manière correcte, en C#, de représenter une structure de données qui, "logiquement" (c'est-à-dire, "pour l'esprit humain"), n'est qu'un list de things avec quelques cloches et sifflets ?

hérite de List<T> toujours inacceptable ? Quand est-ce que c'est acceptable ? Pourquoi/pourquoi pas ? Que doit prendre en compte un programmeur lorsqu'il décide d'hériter de List<T> ou pas ?

115 votes

Votre question est donc de savoir quand utiliser l'héritage (un carré est un rectangle parce qu'il est toujours raisonnable de fournir un carré lorsqu'un rectangle générique a été demandé) et quand utiliser la composition (une équipe de football a une liste de joueurs de football, en plus d'autres propriétés qui sont tout aussi "fondamentales" pour elle, comme un nom) ? Les autres programmeurs seraient-ils confus si, ailleurs dans le code, vous leur transmettiez un FootballTeam alors qu'ils s'attendaient à une simple liste ?

7 votes

@FlightOdyssey Il semble qu'en pratique, ma question se révèle être "Pourquoi la composition est-elle meilleure que l'héritage lorsqu'il s'agit d'étendre les fonctionnalités d'une application ? List ?" Je considère que la "liste des joueurs" est une propriété fondamentale d'une équipe de football, au-dessus de toutes les autres, comme le nom ou le budget. Il peut exister une équipe qui n'a pas de nom (les enfants du quartier qui forment deux équipes pour jouer un après-midi) mais une équipe sans joueurs n'a pas de sens pour moi. Quant à être surpris, je ne sais pas. J'ai du mal à imaginer que quelqu'un puisse être surpris par un tel héritage, mais j'ai l'impression que quelque chose m'échappe.

1 votes

1787voto

Eric Lippert Points 300275

Il y a quelques bonnes réponses ici. J'y ajouterais les points suivants.

Quelle est la manière correcte, en C#, de représenter une structure de données qui, "logiquement" (c'est-à-dire "pour l'esprit humain"), n'est qu'une liste de choses avec quelques cloches et sifflets ?

Demandez à dix personnes non informaticiennes qui connaissent l'existence du football de remplir les cases vides :

Une équipe de football est un type particulier de _____.

Est-ce que quelqu'un ont-ils dit "liste de joueurs de football avec quelques cloches et sifflets", ou ont-ils tous dit "équipe sportive" ou "club" ou "organisation" ? Votre idée qu'une équipe de football est un type particulier de liste de joueurs est dans votre esprit humain et votre esprit humain seul.

List<T> est un mécanisme . L'équipe de football est une objet d'entreprise -- c'est-à-dire un objet qui représente un certain concept qui est dans le domaine des affaires du programme. Ne les mélangez pas ! Une équipe de football est une sorte de équipe ; il a un roster, un roster est une liste de joueurs . Un fichier n'est pas un un type particulier de liste de joueurs . Une liste est une liste de joueurs. Créez donc une propriété appelée Roster qui est un List<Player> . Et le rendre ReadOnlyList<Player> pendant que vous y êtes, à moins que vous ne croyiez que tous ceux qui connaissent une équipe de football ont le droit de supprimer des joueurs de la liste.

hérite de List<T> toujours inacceptable ?

Inacceptable pour qui ? Moi ? Non.

Quand est-ce que c'est acceptable ?

Quand vous construisez un mécanisme qui prolonge le List<T> mécanisme .

Que doit prendre en compte un programmeur, lorsqu'il décide d'hériter de List<T> ou pas ?

Est-ce que je construis un mécanisme ou un objet d'entreprise ?

Mais c'est beaucoup de code ! Qu'est-ce que je gagne pour tout ce travail ?

Vous avez passé plus de temps à rédiger votre question qu'il ne vous en aurait fallu pour rédiger les méthodes d'acheminement pour les membres concernés de l'UE. List<T> cinquante fois plus. Il est clair que vous n'avez pas peur de la verbosité, et nous parlons ici d'une très petite quantité de code ; c'est un travail de quelques minutes.

UPDATE

J'y ai réfléchi un peu plus et il y a une autre raison de ne pas modeler une équipe de football comme une liste de joueurs. En fait, c'est peut-être une mauvaise idée de modéliser une équipe de football en tant que ayant une liste de joueurs également. Le problème avec une équipe en tant que liste de joueurs, c'est que ce que vous obtenez est une instantané de l'équipe à un moment donné . Je ne sais pas quelle est votre analyse de rentabilité pour cette classe, mais si j'avais une classe qui représentait une équipe de football, je voudrais lui poser des questions comme "combien de joueurs des Seahawks ont manqué des matchs pour cause de blessure entre 2003 et 2013 ?" ou "Quel joueur de Denver qui jouait auparavant pour une autre équipe a eu la plus grande augmentation de yards courus d'une année sur l'autre ?" ou " Est-ce que les Piggers sont allés jusqu'au bout cette année ? "

C'est-à-dire qu'une équipe de football me semble bien modélisée en tant que un recueil de faits historiques comme le moment où un joueur a été recruté, blessé, a pris sa retraite, etc. Il est évident que la liste actuelle des joueurs est une donnée importante qui doit être mise en avant, mais il se peut que vous souhaitiez faire d'autres choses intéressantes avec cet objet qui nécessitent une perspective plus historique.

105 votes

Bien que les autres réponses aient été très utiles, je pense que celle-ci répond le plus directement à mes préoccupations. Pour ce qui est d'exagérer la quantité de code, vous avez raison de dire qu'il ne s'agit pas de beaucoup de travail au final, mais je m'embrouille très facilement lorsque j'inclus du code dans mon programme sans comprendre pourquoi je l'inclus.

143 votes

@Superbest : Heureux d'avoir pu aider ! Tu as raison d'écouter ces doutes ; si tu ne comprends pas pourquoi tu écris un certain code, soit tu le découvres, soit tu écris un code différent que tu comprends.

1 votes

@EricLippert "Vous avez passé plus de temps à taper votre question qu'il ne vous en aurait fallu pour écrire cinquante fois les méthodes de transfert pour les membres concernés de List<T>." -- Cela fait un moment que je n'ai pas travaillé dans Visual Studio, mais lorsque je travaille en Java avec Eclipse, l'environnement peut autogénérer ces méthodes (Source/Générer des méthodes de délégation/sélectionner la liste/finir : 4 clics de souris). VS ne dispose-t-il pas d'une option permettant de faire cela ?

273voto

Simon Whitehead Points 27669

Enfin, certains suggèrent d'envelopper la Liste dans quelque chose :

C'est la bonne façon de faire. "Inutilement verbeux" est une mauvaise façon de voir les choses. Cela a un sens explicite lorsque vous écrivez my_team.Players.Count . Vous voulez compter les joueurs.

my_team.Count

..ne veut rien dire. Compter quoi ?

Votre conception est brisée lorsque vous voulez hériter de List . Votre équipe n'est pas une liste c'est une entité englobante qui possède d'autres entités. Une équipe possède des joueurs, donc les joueurs devraient en faire partie (un membre).

Si vous êtes vraiment si vous vous inquiétez du fait que ce soit "inutilement verbeux", vous pouvez toujours exposer les propriétés de l'équipe :

public int PlayerCount {
    get {
        return Players.Count;
    }
}

..qui devient :

my_team.PlayerCount

Cela a un sens maintenant et adhère à La loi de Déméter .

Vous devriez également envisager d'adhérer à la Principe de réutilisation des composites . En héritant de List<T> vous dites qu'une équipe est un liste de joueurs et d'exposer des méthodes inutiles à partir de celle-ci. C'est incorrect - comme vous l'avez dit, une équipe est plus qu'une liste de joueurs : elle a un nom, des managers, des membres du conseil d'administration, des entraîneurs, un personnel médical, des plafonds salariaux, etc. En faisant en sorte que votre classe d'équipe contienne une liste de joueurs, vous dites "une équipe a un liste de joueurs", mais il peut aussi avoir d'autres choses.

199voto

m-y Points 12871

Wow, votre message contient toute une série de questions et de points. La plupart des raisonnements de Microsoft sont tout à fait pertinents. Commençons par tout ce qui concerne List<T>

  • List<T> est hautement optimisé. Son usage principal est d'être utilisé comme membre privé d'un objet.
  • Microsoft ne l'a pas scellé parce que parfois, on peut vouloir créer une classe qui a un nom plus convivial : class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... } . Maintenant, c'est aussi simple que de faire var list = new MyList<int, string>(); .
  • CA1002 : Ne pas exposer les listes génériques : Fondamentalement, même si vous prévoyez d'utiliser cette application en tant que développeur unique, il vaut la peine de développer avec de bonnes pratiques de codage, afin qu'elles deviennent inculquées en vous et une seconde nature. Vous êtes toujours autorisé à exposer la liste en tant que IList<T> si vous avez besoin d'un consommateur pour avoir une liste indexée. Cela vous permet de modifier ultérieurement l'implémentation au sein d'une classe.
  • Microsoft a fait Collection<T> très générique parce que c'est un concept générique... le nom dit tout ; c'est juste une collection. Il existe des versions plus précises telles que SortedCollection<T> , ObservableCollection<T> , ReadOnlyCollection<T> etc., chacun d'entre eux mettant en œuvre IList<T> mais no List<T> .
  • Collection<T> permet de remplacer les membres (c'est-à-dire Ajouter, Supprimer, etc.) parce qu'ils sont virtuels. List<T> ne le fait pas.
  • La dernière partie de votre question est très pertinente. Une équipe de football est plus qu'une simple liste de joueurs, elle devrait donc être une classe qui contient cette liste de joueurs. Pensez à Composition et héritage . Une équipe de football a une liste de joueurs (un roster), ce n'est pas une liste de joueurs.

Si j'écrivais ce code, la classe ressemblerait probablement à quelque chose comme ça :

public class FootballTeam<T>//generic class
{
    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    // Yes. I used LINQ here. This is so I don't have to worry about
    // _roster.Length vs _roster.Count vs anything else.
    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

0 votes

FootballGroup : List<FootballPlayers>, cela me semble tout à fait acceptable et très efficace. Il n'y a pas eu de véritables raisons techniques expliquant pourquoi cette construction ne méritait pas le droit de vivre, seulement des arguments phylosophiques.

0 votes

Je suis sceptique quant à votre argument selon lequel List est descellé pour permettre des noms plus conviviaux. C'est ce que using Les pseudonymes sont pour.

5 votes

@Brian : Sauf que vous ne pouvez pas créer un générique en utilisant un alias, tous les types doivent être concrets. Dans mon exemple, List<T> est étendu en tant que classe interne privée qui utilise des génériques pour simplifier l'utilisation et la déclaration de List<T> . Si l'extension était simplement class FootballRoster : List<FootballPlayer> { } alors oui, j'aurais pu utiliser un alias à la place. Je pense qu'il est utile de noter que vous pouvez ajouter des membres/fonctionnalités supplémentaires, mais que cela est limité puisque vous ne pouvez pas remplacer les membres importants de la fonction List<T> .

171voto

MeNoTalk Points 725
class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

Le code précédent signifie : une bande de gars de la rue qui jouent au football, et il se trouve qu'ils ont un nom. Quelque chose comme :

People playing football

Quoi qu'il en soit, ce code (tiré de la réponse de m-y)

public class FootballTeam
{
    // A team's name
    public string TeamName; 

    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

Signifie : il s'agit d'une équipe de football qui a une direction, des joueurs, des administrateurs, etc. Quelque chose comme ça :

Image showing members (and other attributes) of the Manchester United team

C'est ainsi que votre logique est présentée en images

15 votes

Je pense que votre deuxième exemple est un club de football, et non une équipe de football. Un club de football a des managers, des administrateurs, etc. De cette source : "Une équipe de football est le nom collectif donné à un groupe de joueurs ... Ces équipes pourraient être sélectionnées pour jouer un match contre une équipe adverse, pour représenter un club de football...". Je suis donc d'avis qu'une équipe est une liste de joueurs (ou peut-être plus exactement une collection de joueurs).

5 votes

Plus optimisé private readonly List<T> _roster = new List<T>(44);

2 votes

Il est nécessaire d'importer System.Linq espace de noms.

135voto

Tim B Points 19851

C'est un exemple classique de composition vs héritage .

Dans ce cas précis :

L'équipe est-elle une liste de joueurs avec un comportement ajouté ?

o

L'équipe est-elle un objet à part entière qui contient une liste de joueurs ?

En étendant la liste, vous vous limitez de plusieurs façons :

  1. Vous ne pouvez pas restreindre l'accès (par exemple, empêcher les personnes de modifier la liste). Vous disposez de toutes les méthodes de la liste, que vous en ayez besoin ou non.

  2. Que se passe-t-il si vous voulez avoir des listes d'autres choses aussi. Par exemple, les équipes ont des entraîneurs, des managers, des supporters, des équipements, etc. Certains de ces éléments pourraient bien être des listes à part entière.

  3. Vous limitez vos possibilités d'héritage. Par exemple, vous pourriez vouloir créer un objet générique Team, puis avoir BaseballTeam, FootballTeam, etc. qui héritent de cet objet. Pour hériter de List, vous devez faire l'héritage de Team, mais cela signifie alors que tous les différents types d'équipe sont obligés d'avoir la même implémentation de ce roster.

Composition - inclure un objet donnant le comportement que vous voulez à l'intérieur de votre objet.

Héritage - votre objet devient une instance de l'objet qui a le comportement que vous voulez.

Les deux ont leur utilité, mais dans ce cas précis, la composition est préférable.

1 votes

En développant le point 2, cela n'a pas de sens pour une liste de avoir un Liste.

0 votes

Tout d'abord, la question n'a rien à voir avec la composition ou l'héritage. La réponse est que le PO ne veut pas implémenter un type de liste plus spécifique, donc il ne devrait pas étendre List<>. Je suis tordu parce que vous avez des scores très élevés sur stackoverflow et devrait savoir cela clairement et les gens font confiance à ce que vous dites, donc maintenant un min de 55 personnes qui ont upvoted et dont l'idée est confuse croire soit une façon ou l'autre est ok pour construire un système, mais clairement ce n'est pas ! #no-rage

7 votes

@sam Il veut le comportement de la liste. Il a deux choix, il peut étendre la liste (héritage) ou il peut inclure une liste dans son objet (composition). Peut-être avez-vous mal compris une partie de la question ou de la réponse, plutôt que 55 personnes aient tort et vous raison :)

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