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 (unint
). .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 estList<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 :
-
Cela rend mon code inutilement verbeux. Je dois maintenant appeler
my_team.Players.Count
au lieu de seulementmy_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 ? -
Ç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
Voir aussi stackoverflow.com/questions/4353203/
94 votes
Et si je vous disais qu'une équipe de football est une entreprise commerciale qui possède une liste de contrats de joueurs au lieu d'une simple liste de joueurs avec quelques propriétés supplémentaires ?
99 votes
C'est vraiment très simple. Une équipe de football est-elle une liste de joueurs ? Évidemment non, car comme vous le dites, il existe d'autres propriétés pertinentes. Une équipe de football A-t-elle une liste de joueurs ? Oui, elle doit donc contenir une liste. À part cela, la composition est simplement préférable à l'héritage en général, car elle est plus facile à modifier par la suite. Si vous trouvez l'héritage et la composition logiques en même temps, choisissez la composition.
7 votes
my_team.CountFootballs
,my_team.CountSpareUniforms
...16 votes
@FlightOdyssey : Soyez prudent avec l'analogie du carré est une sorte de rectangle ; cela ne fonctionne que si le carré et le rectangle sont valeurs immuables . S'ils sont mutables, vous avez des problèmes.
52 votes
@FlightOdyssey : Supposons que vous ayez une méthode SquashRectangle qui prend un rectangle, divise sa hauteur par deux et double sa largeur. Maintenant, passez un rectangle qui se produit pour être 4x4 mais est de type rectangle, et un carré qui est 4x4. Quelles sont les dimensions de l'objet après l'appel de SquashRectangle ? Pour le rectangle, il est clair qu'il est de 2x8. Si le carré est 2x8 alors ce n'est plus un carré ; s'il n'est pas 2x8 alors la même opération sur la même forme produit un résultat différent. La conclusion est qu'un carré mutable est no une sorte de rectangle.
5 votes
@EricLippert Clairement,
Squash()
devrait simplement être une méthode virtuelle sur leRectangle
et vous obtenez unNotSupportedException
lorsque vous essayez d'appeler la méthode sur unSquare
qui se fait passer pour unRectangle
. Pour éviter cette exception, j'ajouterais alors un fichier virtuelIsSquashable
à la classe, convertissant ainsi l'exception contrariante en une exception stupide (puisqu'ils auraient dû simplement vérifier le drapeau !) Ensuite, je... oh non, j'ai louché. La conception OO est difficile.2 votes
@EricLippert Ainsi tout héritage ne fonctionnera pas, car une vraie sous-classe a toujours certaines fonctionnalités fermées, juste parce qu'elle n'est qu'un sous-ensemble du parent. Seulement nous ne réalisons jamais toutes les fonctions et ce n'est pas évident. Donc, votre pensée mène à l'absurde, et est fausse. Oui, ces fonctions qui ne correspondent pas à la sous-classe doivent être surchargées.
41 votes
@Gangnus : Votre affirmation est tout simplement fausse ; une vraie sous-classe doit avoir une fonctionnalité qui est une superset de la superclasse. A
string
est tenu de faire tout ce qu'unobject
peut faire et plus .2 votes
@EricLippert Point assez juste concernant
Rectangle
etSquare
s. Si j'ai utilisé cet exemple particulier, ce n'est pas pour dire que dans les pays en voie de développement, il n'y a pas d'autre solution. chaque structure de classe possible aSquare
doit être implémentée en tant que sous-classe d'uneRectangle
Le but est de donner un exemple simple pour illustrer le concept général de l'héritage sans se perdre dans de longues explications et/ou une terminologie obscure.6 votes
List<T> ont trop de méthodes et de propriétés qui ne sont pas pertinentes pour votre entreprise . Vous pouvez peut-être déclarer l'équipe de football comme une interface. Une implémentation basée sur une liste est une possibilité.
1 votes
Les réponses ici font un point valable (voir notamment celles d'Eric Lippert et de Sam Leach). En effet, une équipe de football n'est pas un liste des joueurs. Ce n'est qu'un aspect de la question. Une classe qui pourrait hériter de
List<T>
qui a du sens serait si son nomFootballPlayerCollection
(les puristes me corrigeront en disant queCollection<T>
est un meilleur choix, ok, mais vous avez compris).2 votes
Légèrement hors sujet (mais qui vaut quand même la peine d'être mentionné IMHO) :
List<T>
n'est pas une bonne solution car elle peut contenir des doublons. Une équipe de football peut-elle vraiment avoir le même joueur plus d'une fois ! Ce serait le cas siFootballTeam
hérité de ou contenant unList<T>
. Je vous recommande d'opter pour un type de collecte offrant des garanties plus strictes, tel queISet<T>
.2 votes
Je ne suis pas d'accord avec les deux "problèmes" que vous avez énumérés. Premièrement, si j'ai une équipe et 11 joueurs, je m'attends à ce que my_team.Players.Count = 11, Players.Count (un tableau) = 11 et my_team.Count = 1 (en fait, my_team ne devrait même pas avoir de méthode Count). Deuxièmement, une équipe de football ne se résume pas aux joueurs. Elle possède diverses propriétés, telles que l'état dans lequel elle est établie, etc. Ainsi, il est plus approprié de la modéliser en tant que mon_équipe, qui inclut les joueurs comme propriété. class FootballTeam { public List<FootballPlayer> Players ; } est la façon naturelle de la modéliser, même sans que MS ait à vous donner une ligne directrice.
0 votes
Regardez les réponses de ce post. stackoverflow.com/questions/400135/c-sharp-listt-or-ilistt
8 votes
Wow, les réponses ont déraillé ici. La modélisation est toujours délicate, mais en fait la question est de savoir quand et quand ne pas hériter de List, et pourquoi les directives MS suggèrent de ne pas le faire. Répondez à la question posée, s'il vous plaît.
0 votes
"On n'ajoute pas un livre aux livres d'une bibliothèque, on ajoute un livre à une bibliothèque", vous devriez peut-être regarder ce qu'est "Meotnymy".