[c#] Perché non ereditare dalla lista <T>?


Answers

Infine, alcuni suggeriscono di avvolgere la lista in qualcosa:

Questo è il modo corretto. "Wordless inutilmente" è un brutto modo di guardare a questo. Ha un significato esplicito quando scrivi my_team.Players.Count . Vuoi contare i giocatori.

my_team.Count

..non significa niente. Conta cosa?

Una squadra non è una lista - il consistere di più di un semplice elenco di giocatori. Una squadra possiede i giocatori, quindi i giocatori dovrebbero farne parte (un membro).

Se sei davvero preoccupato che sia eccessivamente prolisso, puoi sempre esporre le proprietà del team:

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

..che diventa:

my_team.PlayerCount

Questo ha un significato ora e aderisce a The Law Of Demeter .

Dovresti anche considerare di aderire al principio del riutilizzo composito . Tramite l'ereditarietà di List<T> , stai dicendo che una squadra è una lista di giocatori e che ne espone metodi inutili. Questo non è corretto - come hai affermato, una squadra è più di una lista di giocatori: ha un nome, manager, membri del consiglio di amministrazione, allenatori, personale medico, stipendi, ecc. Facendo in modo che la tua classe della squadra contenga un elenco di giocatori, Stai dicendo "Una squadra ha una lista di giocatori", ma può anche avere altre cose.

Question

Quando pianifico i miei programmi, spesso inizio con una catena di pensieri come questa:

Una squadra di calcio è solo una lista di giocatori di calcio. Pertanto, dovrei rappresentarlo con:

var football_team = new List<FootballPlayer>();

L'ordine di questa lista rappresenta l'ordine in cui i giocatori sono elencati nel roster.

Ma mi rendo conto più tardi che le squadre hanno anche altre proprietà, oltre alla semplice lista di giocatori, che devono essere registrate. Ad esempio, il totale parziale dei punteggi in questa stagione, il budget corrente, i colori uniformi, una string rappresenta il nome della squadra, ecc.

Allora penso:

Ok, una squadra di football è come una lista di giocatori, ma in aggiunta ha un nome (una string ) e un totale parziale di punteggi (un int ). .NET non fornisce una classe per la memorizzazione delle squadre di calcio, quindi farò la mia classe. La struttura esistente più simile e rilevante è List<FootballPlayer> , quindi erediterò da essa:

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

Ma si scopre che una linea guida dice che non si dovrebbe ereditare dalla List<T> . Sono completamente confuso da questa linea guida sotto due aspetti.

Perchè no?

Apparentemente List è in qualche modo ottimizzato per le prestazioni . Come mai? Quali problemi di prestazioni dovrei causare se estendo List ? Cosa si romperà esattamente?

Un'altra ragione che ho visto è che List è fornito da Microsoft e non ho alcun controllo su di esso, quindi non posso cambiarlo più tardi, dopo aver esposto una "API pubblica" . Ma faccio fatica a capirlo. Che cos'è un'API pubblica e perché dovrei preoccuparmi? Se il mio progetto attuale non ha e non è probabile che abbia mai questa API pubblica, posso ignorare tranquillamente questa linea guida? Se eredito da List e risulta che ho bisogno di un'API pubblica, quali difficoltà dovrò avere?

Perché è ancora importante? Una lista è una lista. Cosa potrebbe cambiare? Cosa potrei voler cambiare?

E infine, se Microsoft non voleva che ereditassi da List , perché non hanno sealed la classe?

Cos'altro dovrei usare?

Apparentemente, per le collezioni personalizzate, Microsoft ha fornito una classe Collection che dovrebbe essere estesa anziché List . Ma questa classe è molto semplice e non ha molte cose utili, come ad esempio AddRange . La risposta di jvitor83 fornisce un razionale di prestazioni per quel particolare metodo, ma come è un AddRange lento non migliore di nessun AddRange ?

Ereditare dalla Collection è molto più lavoro che ereditare dalla List , e non vedo alcun beneficio. Sicuramente Microsoft non mi direbbe di fare del lavoro extra senza motivo, quindi non posso fare a meno di sentirmi in qualche modo frainteso e l'ereditare Collection non è la soluzione giusta per il mio problema.

Ho visto suggerimenti come l'implementazione di IList . Solo no. Sono dozzine di righe di codice boilerplate che non mi fanno guadagnare nulla.

Infine, alcuni suggeriscono di avvolgere la List in qualcosa:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Ci sono due problemi con questo:

  1. Rende il mio codice inutilmente dettagliato. Ora devo chiamare my_team.Players.Count anziché solo my_team.Count . Fortunatamente, con C # posso definire gli indicizzatori per rendere l'indicizzazione trasparente e inoltrare tutti i metodi List interno ... Ma questo è un sacco di codice! Cosa ottengo per tutto ciò che funziona?

  2. Semplicemente non ha alcun senso. Una squadra di calcio non "ha" una lista di giocatori. È la lista dei giocatori. Non dici "John McFootballer è entrato nei giocatori di SomeTeam". Dici "John si è unito a SomeTeam". Non si aggiunge una lettera a "caratteri di una stringa", si aggiunge una lettera a una stringa. Non aggiungi un libro ai libri di una biblioteca, aggiungi un libro a una biblioteca.

Mi rendo conto che ciò che accade "sotto il cofano" si può dire che è "aggiungere la lista interna di X a Y", ma questo sembra un modo molto controintuitivo di pensare al mondo.

La mia domanda (riassunta)

Qual è il modo corretto C # di rappresentare una struttura dati, che, "logicamente" (vale a dire, "per la mente umana") è solo una list di things con poche campane e fischietti?

L'ereditarietà di List<T> sempre inaccettabile? Quando è accettabile? Perché perché no? Cosa deve considerare un programmatore quando decide se ereditare dall'elenco List<T> o no?




Prima di tutto, ha a che fare con l'usabilità. Se si utilizza l'ereditarietà, la classe Team esporrà comportamenti (metodi) progettati esclusivamente per la manipolazione degli oggetti. Ad esempio, i AsReadOnly() o CopyTo(obj) non hanno senso per l'oggetto team. Invece del metodo AddRange(items) probabilmente vorresti un metodo AddPlayers(players) più descrittivo.

Se si desidera utilizzare LINQ, l'implementazione di un'interfaccia generica come ICollection<T> o IEnumerable<T> avrebbe più senso.

Come già detto, la composizione è il modo giusto per farlo. Basta implementare un elenco di giocatori come variabile privata.




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
{
    ...
}



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.




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.




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.




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

Codice precedente significa: un gruppo di ragazzi della strada che giocano a calcio, e capita di avere un nome. Qualcosa di simile a:

Ad ogni modo, questo codice (dalla mia risposta)

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.
}

Mezzi: questa è una squadra di calcio che ha dirigenti, giocatori, amministratori, ecc. Qualcosa del tipo:

Ecco come viene presentata la tua logica nelle immagini ...




Come tutti hanno sottolineato, una squadra di giocatori non è una lista di giocatori. Questo errore è causato da molte persone ovunque, forse a vari livelli di esperienza. Spesso il problema è sottile e occasionalmente molto grave, come in questo caso. Tali progetti sono cattivi perché questi violano il Principio di sostituzione di Liskov . Internet ha molti buoni articoli che spiegano questo concetto, ad es. http://en.wikipedia.org/wiki/Liskov_substitution_principle

In sintesi, ci sono due regole da conservare in una relazione padre / figlio tra classi:

  • un bambino non dovrebbe richiedere caratteristiche inferiori a quelle che definiscono completamente il genitore.
  • un genitore non dovrebbe richiedere alcuna caratteristica oltre a ciò che definisce completamente il bambino.

In altre parole, un genitore è una definizione necessaria di un bambino e un figlio è una definizione sufficiente di un genitore.

Ecco un modo per pensare attraverso una soluzione e applicare il principio di cui sopra che dovrebbe aiutare a evitare un simile errore. Si dovrebbe testare l'ipotesi verificando se tutte le operazioni di una classe genitore sono valide per la classe derivata sia strutturalmente che semanticamente.

  • Una squadra di calcio è una lista di giocatori di calcio? (Tutte le proprietà di un elenco si applicano a una squadra con lo stesso significato)
    • Una squadra è una raccolta di entità omogenee? Sì, la squadra è una collezione di giocatori
    • L'ordine di inclusione dei giocatori è descrittivo dello stato della squadra e il team assicura che la sequenza venga conservata a meno che non venga esplicitamente modificato? No e No
    • I giocatori dovrebbero essere inclusi / eliminati in base alla loro posizione sequenziale nella squadra? No

Come vedi, solo la prima caratteristica di una lista è applicabile a una squadra. Quindi una squadra non è una lista. Un elenco sarebbe un dettaglio di implementazione di come gestisci il tuo team, quindi dovrebbe essere usato solo per memorizzare gli oggetti del giocatore ed essere manipolato con i metodi della classe Team.

A questo punto vorrei sottolineare che una classe di Team dovrebbe, a mio parere, non essere nemmeno implementata usando una Lista; dovrebbe essere implementato utilizzando una struttura di dati Set (ad esempio HashSet) nella maggior parte dei casi.




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. Questo è tutto. 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 
}



Related