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



13 Answers

Infine, alcuni suggeriscono di avvolgere la lista in qualcosa:

Questo è il modo corretto. "Stranamente verboso" è 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 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.

c# .net list oop inheritance

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?




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 dovrebbero testare le 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.




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.




Una squadra di calcio non è una lista di giocatori di calcio. Una squadra di calcio è composta da una lista di giocatori di calcio!

Questo è logicamente sbagliato:

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

e questo è corretto:

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



Fammi riscrivere la tua domanda. quindi potresti vedere il soggetto da una prospettiva diversa.

Quando ho bisogno di rappresentare una squadra di calcio, capisco che è fondamentalmente un nome. Come: "Le aquile"

string team = new string();

Poi più tardi ho realizzato che anche i team hanno giocatori.

Perché non posso semplicemente estendere il tipo di stringa in modo che tenga anche un elenco di giocatori?

Il tuo punto di accesso al problema è arbitrario. Provate a pensare che cosa fa una squadra ha (proprietà), non è quello che è .

Dopo averlo fatto, puoi vedere se condivide le proprietà con altre classi. E pensa all'eredità.




Permette alle persone di dire

myTeam.subList(3, 5);

ha senso? In caso contrario, non dovrebbe essere una lista.




Ci sono molte risposte eccellenti qui, ma voglio toccare qualcosa che non ho visto menzionato: il design orientato agli oggetti riguarda il potenziamento degli oggetti .

Vuoi incapsulare tutte le tue regole, lavoro aggiuntivo e dettagli interni all'interno di un oggetto appropriato. In questo modo altri oggetti che interagiscono con questo non devono preoccuparsi di tutto questo. In realtà, si vuole fare un ulteriore passo avanti e impedire attivamente ad altri oggetti di aggirare questi interni.

Quando si eredita da List, tutti gli altri oggetti possono vederti come una lista. Hanno accesso diretto ai metodi per aggiungere e rimuovere giocatori. E avrai perso il controllo; per esempio:

Supponiamo di voler differenziare quando un giocatore se ne va sapendo se è andato in pensione, ha rassegnato le dimissioni o è stato licenziato. È possibile implementare un RemovePlayermetodo che richiede un enum di input appropriato. Tuttavia, ereditando da List, si sarebbe in grado di impedire l'accesso diretto a Remove, RemoveAlle persino Clear. Di conseguenza, hai effettivamente disimpegnato la tua FootballTeamclasse.

Ulteriori riflessioni sull'incapsulamento ... Hai sollevato la seguente preoccupazione:

Rende il mio codice inutilmente dettagliato. Ora devo chiamare my_team.Players.Count anziché solo my_team.Count.

Hai ragione, sarebbe inutilmente prolisso per tutti i clienti che usano la tua squadra. Tuttavia, questo problema è molto piccolo in confronto al fatto che hai esposto List Playersa tutti e diversi in modo da poter giocare con la tua squadra senza il tuo consenso.

Continui a dire:

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

Ti stai sbagliando per la prima parte: cancella la parola "elenco" ed è evidente che una squadra ha giocatori.
Tuttavia, hai colpito il chiodo sulla testa con il secondo. Non vuoi che i clienti chiamino ateam.Players.Add(...). Li vuoi chiamare ateam.AddPlayer(...). E la tua implementazione sarebbe (forse tra le altre cose) chiamata Players.Add(...)internamente.

Spero che tu possa vedere quanto sia importante l'incapsulamento per l'obiettivo di potenziare i tuoi oggetti. Vuoi consentire a ogni classe di svolgere bene il proprio lavoro senza temere interferenze da altri oggetti.




Questo mi ricorda il "È un" contro "ha un" compromesso ". A volte è più facile e ha più senso ereditare direttamente da una super classe. Altre volte ha più senso creare una classe autonoma e includere la classe che avresti ereditato come variabile membro. È ancora possibile accedere alle funzionalità della classe ma non sono legati all'interfaccia o ad altri vincoli che potrebbero derivare dall'erogazione della classe.

Che fai? Come con molte cose ... dipende dal contesto. La guida che userei è che per ereditare da un'altra classe ci dovrebbe veramente essere una relazione "è una". Quindi se scrivi una classe chiamata BMW, potrebbe ereditare da Car perché una BMW è veramente un'auto. Una classe Horse può ereditare dalla classe Mammal perché un cavallo è effettivamente un mammifero nella vita reale e qualsiasi funzionalità del Mammifero dovrebbe essere rilevante per Horse. Ma puoi dire che una squadra è una lista? Da quello che posso dire, non sembra che una squadra sia davvero una "lista". Quindi in questo caso, avrei una lista come variabile membro.




Quello che dicono le linee guida è che l'API pubblica non dovrebbe rivelare la decisione di progettazione interna se si sta utilizzando una lista, un set, un dizionario, un albero o altro. Una "squadra" non è necessariamente una lista. Puoi implementarlo come una lista ma gli utenti della tua API pubblica dovrebbero usare la tua classe sulla base della necessità di conoscere. Ciò ti consente di cambiare la tua decisione e utilizzare una diversa struttura dei dati senza influire sull'interfaccia pubblica.




Anche se non ho un confronto complesso come la maggior parte di queste risposte, vorrei condividere il mio metodo per gestire questa situazione. Estendendo IEnumerable<T>, puoi consentire alla tua Teamclasse di supportare le estensioni di query di Linq, senza esporre pubblicamente tutti i metodi e le proprietà di 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
{
    ...
}



Volevo solo aggiungere che Bertrand Meyer, l'inventore di Eiffel e design per contratto, avrebbe Teamereditato dall'esterno List<Player>senza nemmeno battere ciglio.

Nel suo libro, Object-Oriented Software Construction , discute l'implementazione di un sistema GUI in cui le finestre rettangolari possono avere finestre figlio. Ha semplicemente Windowereditato da entrambi Rectanglee Tree<Window>riutilizzare l'implementazione.

Tuttavia, C # non è Eiffel. Quest'ultimo supporta l'ereditarietà multipla e la rinomina delle funzionalità . In C #, quando si sottoclasse, si eredita sia l'interfaccia che l'implementazione. È possibile sovrascrivere l'implementazione, ma le convenzioni di chiamata vengono copiate direttamente dalla superclasse. In Eiffel, tuttavia, è possibile modificare i nomi dei metodi pubblici, in modo da poter rinominare Adde Removeda Hiree Firenel proprio Team. Se un'istanza di Teamè upcast torna a List<Player>, il chiamante utilizza Adde Removemodificarlo, ma i vostri metodi virtuali Hiree Firesarà chiamato.




Preferisci interfacce su classi

Le classi dovrebbero evitare di derivare dalle classi e implementare invece le interfacce minime necessarie.

Interruzioni di eredità Incapsulamento

Derivando dall'incapsulamento delle pause delle lezioni :

  • espone i dettagli interni su come viene implementata la tua collezione
  • dichiara un'interfaccia (set di funzioni e proprietà pubbliche) che potrebbe non essere appropriata

Tra l'altro questo rende più difficile il refactoring del codice.

Le classi sono un dettaglio di implementazione

Le classi sono dettagli di implementazione che dovrebbero essere nascosti da altre parti del codice. In breve a System.Listè un'implementazione specifica di un tipo di dati astratto, che può o non può essere appropriato ora e in futuro.

Concettualmente il fatto che il System.Listtipo di dati sia chiamato "elenco" è un po 'un falso rosso. A System.List<T>è una collezione ordinata mutevole che supporta operazioni O (1) ammortizzate per aggiungere, inserire e rimuovere elementi e operazioni O (1) per recuperare il numero di elementi o ottenere e impostare l'elemento per indice.

Più piccola è l'interfaccia più flessibile è il codice

Quando si progetta una struttura dati, più semplice è l'interfaccia, più flessibile è il codice. Guarda solo quanto è potente LINQ per dimostrarlo.

Come scegliere le interfacce

Quando pensi "lista" dovresti iniziare dicendo a te stesso: "Devo rappresentare una collezione di giocatori di baseball". Quindi diciamo che decidi di modellarlo con una classe. Quello che dovresti fare prima è decidere qual è la quantità minima di interfacce che questa classe dovrà esporre.

Alcune domande che possono aiutare a guidare questo processo:

  • Devo avere il conteggio? Se non considerare l'implementazioneIEnumerable<T>
  • Questa collezione cambierà dopo essere stata inizializzata? Se non lo consideri IReadonlyList<T>.
  • È importante poter accedere agli oggetti per indice? Tenere contoICollection<T>
  • L'ordine in cui aggiungo elementi alla collezione è importante? Forse è un ISet<T>?
  • Se davvero vuoi queste cose, vai avanti e attua IList<T>.

In questo modo non accoppierà altre parti del codice ai dettagli di implementazione della tua collezione di giocatori di baseball e sarai libero di cambiare il modo in cui è implementato, purché tu rispetti l'interfaccia.

Con questo approccio scoprirai che il codice diventa più facile da leggere, refactoring e riutilizzo.

Note sull'elusione di Boilerplate

Implementare interfacce in un moderno IDE dovrebbe essere facile. Fare clic con il tasto destro e selezionare "Implementa interfaccia". Quindi inoltra tutte le implementazioni a una classe membro, se necessario.

Detto questo, se trovi che stai scrivendo molte regole, è potenzialmente perché stai esponendo più funzioni di quanto dovresti essere. È la stessa ragione per cui non dovresti ereditare da una classe.

Puoi anche progettare interfacce più piccole che hanno senso per la tua applicazione, e forse solo un paio di funzioni di estensione dell'helper per mappare tali interfacce a qualsiasi altra di cui hai bisogno. Questo è l'approccio che ho preso nella mia IArrayinterfaccia per la libreria LinqArray .




Related