[C#] Pourquoi ne pas hériter de List <T>?


Answers

Enfin, certains suggèrent d'encapsuler la liste dans quelque chose:

C'est la bonne façon. "Inutile verbeux" est une mauvaise façon de voir cela. Il a une signification explicite lorsque vous écrivez my_team.Players.Count . Vous voulez compter les joueurs.

my_team.Count

..ne signifie rien. Comptez quoi?

Une équipe n'est pas une liste - le composé de plus que juste une liste de joueurs. Une équipe possède des joueurs, donc les joueurs devraient en faire partie (un membre).

Si vous êtes vraiment inquiet à propos d'être trop verbeux, vous pouvez toujours exposer les propriétés de l'équipe:

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

..which devient:

my_team.PlayerCount

Cela a un sens maintenant et adhère à la loi de Demeter .

Vous devriez également envisager de respecter le principe de réutilisation composite . En héritant de List<T> , vous dites qu'une équipe est une liste de joueurs et qu'elle expose des méthodes inutiles. C'est faux - comme vous l'avez dit, une équipe est plus qu'une liste de joueurs: elle a un nom, des gestionnaires, des membres du conseil, des entraîneurs, du personnel médical, des plafonds salariaux, etc. disons "Une équipe a une liste de joueurs", mais elle peut aussi avoir d'autres choses.

Question

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 le représenter avec:

var football_team = new List<FootballPlayer>();

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

Mais je me rends compte plus tard que les équipes ont également d'autres propriétés, en plus de la simple liste des joueurs, qui doivent être enregistrées. Par exemple, le cumul des scores de cette saison, le budget actuel, les couleurs uniformes, une string représentant le nom de l'équipe, etc.

Alors je pense:

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

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

Mais il s'avère qu'une directive indique que vous ne devriez pas hériter de List<T> . Je suis complètement confus par cette directive à deux égards.

Pourquoi pas?

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

Une autre raison que j'ai vu est que List est fourni par Microsoft, et je n'ai aucun contrôle sur elle, donc je ne peux pas le changer plus tard, après avoir exposé une "API publique" . Mais j'ai du mal à comprendre cela. Qu'est-ce qu'une API publique et pourquoi devrais-je m'en préoccuper? Si mon projet actuel n'a pas et ne risque pas d'avoir cette API publique, puis-je ignorer en toute sécurité cette directive? Si j'hérite de List et qu'il s'avère que j'ai besoin d'une API publique, quelles difficultés aurai-je?

Pourquoi est-ce important? Une liste est une liste. Qu'est-ce qui pourrait éventuellement changer? Que pourrais-je vouloir changer?

Et enfin, si Microsoft ne voulait pas que je hérite de List , pourquoi n'a-t-il pas rendu la classe sealed ?

Quoi d'autre suis-je censé utiliser?

Apparemment, pour les collections personnalisées, Microsoft a fourni une classe Collection qui devrait être étendue au lieu de List . Mais cette classe est très nue et n'a 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 un AddRange lent AddRange pas meilleur que pas AddRange ?

Hériter de la Collection est beaucoup plus de travail que d'hériter de la List , et je ne vois aucun avantage. Sûrement Microsoft ne me dirait pas de faire du travail supplémentaire sans raison, alors je ne peux pas m'empêcher de penser que je suis en train de mal comprendre quelque chose, et hériter de Collection n'est pas la bonne solution pour mon problème.

J'ai vu des suggestions telles que l'implémentation d' IList . Tout simplement pas. C'est des dizaines de lignes de code standard qui ne me rapporte rien.

Enfin, certains suggèrent d'encapsuler la List dans quelque chose:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Il y a deux problèmes avec ceci:

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

  2. Cela n'a tout simplement aucun sens. Une équipe de football n'a "pas" de liste de joueurs. C'est la liste des joueurs. Vous ne dites pas "John McFootballer a rejoint les joueurs de SomeTeam". Vous dites "John a rejoint SomeTeam". Vous n'ajoutez pas de lettre aux "caractères d'une chaîne", vous ajoutez une lettre à une chaîne. Vous n'ajoutez pas de livre aux livres d'une bibliothèque, vous ajoutez un livre à une bibliothèque.

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

Ma question (résumé)

Quelle est la façon correcte de représenter une structure de données en C #, qui, "logiquement" (c'est-à-dire "à l'esprit humain") est juste une list de things avec quelques cloches et sifflets?

L'héritage de List<T> toujours inacceptable? Quand est-ce acceptable? Pourquoi pourquoi pas? Que doit prendre en compte un programmeur lorsqu'il décide de l'héritage de List<T> ou non?




Tout d'abord, cela concerne la convivialité. Si vous utilisez l'héritage, la classe Team exposera un comportement (méthodes) conçu uniquement pour la manipulation d'objets. Par exemple, les AsReadOnly() ou CopyTo(obj) n'ont aucun sens pour l'objet d'équipe. Au lieu de la AddRange(items) , vous voudrez probablement une méthode AddPlayers(players) plus descriptive.

Si vous voulez utiliser LINQ, implémenter une interface générique telle que ICollection<T> ou IEnumerable<T> aurait plus de sens.

Comme mentionné, la composition est la bonne façon de s'y prendre. Implémentez simplement une liste de joueurs en tant que variable privée.




It depends on the behaviour of your "team" object. If it behaves just like a collection, it might be OK to represent it first with a plain List. Then you might start to notice that you keep duplicating code that iterates on the list; at this point you have the option of creating a FootballTeam object that wraps the list of players. The FootballTeam class becomes the home for all the code that iterates on the list of players.

It makes my code needlessly verbose. I must now call my_team.Players.Count instead of just my_team.Count. Thankfully, with C# I can define indexers to make indexing transparent, and forward all the methods of the internal List... But that's a lot of code! What do I get for all that work?

Encapsulation. Your clients need not know what goes on inside of FootballTeam. For all your clients know, it might be implemented by looking the list of players up in a database. They don't need to know, and this improves your design.

It just plain doesn't make any sense. A football team doesn't "have" a list of players. It is the list of players. You don't say "John McFootballer has joined SomeTeam's players". You say "John has joined SomeTeam". You don't add a letter to "a string's characters", you add a letter to a string. You don't add a book to a library's books, you add a book to a library.

Exactly :) you will say footballTeam.Add(john), not footballTeam.List.Add(john). The internal list will not be visible.




What the guidelines say is that the public API should not reveal the internal design decision of whether you are using a list, a set, a dictionary, a tree or whatever. A "team" is not necessarily a list. You may implement it as a list but users of your public API should use you class on a need to know basis. This allows you to change your decision and use a different data structure without affecting the public interface.




What is the correct C# way of representing a data structure...

Remeber, "All models are wrong, but some are useful." - George EP Box

There is no a "correct way", only a useful one.

Choose one that is useful to you and/your users. C'est tout. Develop economically, don't over-engineer. The less code you write, the less code you will need to debug. (read the following editions).

-- Edited

My best answer would be... it depends. Inheriting from a List would expose the clients of this class to methods that may be should not be exposed, primarily because FootballTeam looks like a business entity.

-- Edition 2

I sincerely don't remember to what I was referring on the “don't over-engineer” comment. While I believe the KISS mindset is a good guide, I want to emphasize that inheriting a business class from List would create more problems than it resolves, due abstraction leakage .

On the other hand, I believe there are a limited number of cases where simply to inherit from List is useful. As I wrote in the previous edition, it depends. The answer to each case is heavily influenced by both knowledge, experience and personal preferences.

Thanks to @kai for helping me to think more precisely about the answer.




A football team is not a list of football players. A football team is composed of a list of football players!

This is logically wrong:

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

and this is correct:

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



Comme tout le monde l'a souligné, une équipe de joueurs n'est pas une liste de joueurs. Cette erreur est faite par beaucoup de gens partout, peut-être à différents niveaux d'expertise. Souvent, le problème est subtil et parfois très grave, comme dans ce cas. De telles conceptions sont mauvaises parce qu'elles violent le principe de substitution de Liskov . L'Internet a beaucoup de bons articles expliquant ce concept par exemple, http://en.wikipedia.org/wiki/Liskov_substitution_principle

En résumé, il existe deux règles à conserver dans une relation Parent / Enfant entre les classes:

  • Un enfant ne devrait exiger aucune caractéristique moins que ce qui définit complètement le parent.
  • Un Parent ne devrait pas exiger de caractéristique en plus de ce qui définit complètement l'Enfant.

En d'autres termes, un parent est une définition nécessaire d'un enfant, et un enfant est une définition suffisante d'un parent.

Voici un moyen de réfléchir à la solution et d'appliquer le principe ci-dessus qui devrait aider à éviter une telle erreur. On devrait tester son hypothèse en vérifiant si toutes les opérations d'une classe parente sont valides pour la classe dérivée à la fois structurellement et sémantiquement.

  • Une équipe de football est-elle une liste de joueurs de football? (Est-ce que toutes les propriétés d'une liste s'appliquent à une équipe dans le même sens)
    • Une équipe est-elle une collection d'entités homogènes? Oui, l'équipe est une collection de joueurs
    • L'ordre d'inclusion des joueurs est-il descriptif de l'état de l'équipe et l'équipe s'assure-t-elle que la séquence est conservée à moins d'être explicitement modifiée? Non, et non
    • Les joueurs devraient-ils être inclus / abandonnés en fonction de leur position séquentielle dans l'équipe? Non

Comme vous le voyez, seule la première caractéristique d'une liste est applicable à une équipe. D'où une équipe n'est pas une liste. Une liste serait un détail d'implémentation de la façon dont vous gérez votre équipe, elle ne devrait donc être utilisée que pour stocker les objets joueurs et être manipulée avec des méthodes de classe Team.

À ce stade, je voudrais faire remarquer qu'une classe d'équipe ne devrait, à mon avis, même pas être mise en œuvre en utilisant une liste; il devrait être implémenté en utilisant une structure de données Set (HashSet, par exemple) dans la plupart des cas.




Let me rewrite your question. so you might see the subject from a different perspective.

When I need to represent a football team, I understand that it is basically a name. Like: "The Eagles"

string team = new string();

Then later I realized teams also have players.

Why can't I just extend the string type so that it also holds a list of players?

Your point of entry into the problem is arbitrary. Try to think what does a team have (properties), not what it is .

After you do that, you could see if it shares properties with other classes. And think about inheritance.




This reminds me of the "Is a" versus "has a" tradeoff. Sometimes it is easier and makesmore sense to inherit directly from a super class. Other times it makes more sense to create a standalone class and include the class you would have inherited from as a member variable. You can still access the functionality of the class but are not bound to the interface or any other constraints that might come from inheriting from the class.

Which do you do? As with a lot of things...it depends on the context. The guide I would use is that in order to inherit from another class there truly should be an "is a" relationship. So if you a writing a class called BMW, it could inherit from Car because a BMW truly is a car. A Horse class can inherit from the Mammal class because a horse actually is a mammal in real life and any Mammal functionality should be relevant to Horse. But can you say that a team is a list? From what I can tell, it does not seem like a Team really "is a" List. So in this case, I would have a List as a member variable.




If your class users need all the methods and properties** List has, you should derive your class from it. If they don't need them, enclose the List and make wrappers for methods your class users actually need.

This is a strict rule, if you write a public API , or any other code that will be used by many people. You may ignore this rule if you have a tiny app and no more than 2 developers. This will save you some time.

For tiny apps, you may also consider choosing another, less strict language. Ruby, JavaScript - anything that allows you to write less code.




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

Le code précédent signifie: un groupe de gars de la rue jouant au football, et ils ont un nom. Quelque chose comme:

Quoi qu'il en soit, ce code (de ma réponse)

public class FootballTeam
{
    // 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.
}

Moyens: ceci est une équipe de football qui a la gestion, les joueurs, les admins, etc. Quelque chose comme:

Voici comment est présentée votre logique en images ...




While I don't have a complex comparison as most of these answers do, I would like to share my method for handling this situation. By extending IEnumerable<T> , you can allow your Team class to support Linq query extensions, without publicly exposing all the methods and properties of List<T> .

class Team : IEnumerable<Player>
{
    private readonly List<Player> playerList;

    public Team()
    {
        playerList = new List<Player>();
    }

    public Enumerator GetEnumerator()
    {
        return playerList.GetEnumerator();
    }

    ...
}

class Player
{
    ...
}