Qualcuno può spiegarmi perché dovrei usare IList over List in C #?
Domanda correlata: perché è considerato brutto da esporreList<T>
Qualcuno può spiegarmi perché dovrei usare IList over List in C #?
Domanda correlata: perché è considerato brutto da esporreList<T>
Risposte:
Se stai esponendo la tua classe attraverso una libreria che altri useranno, in genere vorrai esporla tramite interfacce piuttosto che implementazioni concrete. Questo ti aiuterà se decidi di cambiare l'implementazione della tua classe in un secondo momento per usare una diversa classe concreta. In tal caso, gli utenti della tua libreria non dovranno aggiornare il loro codice poiché l'interfaccia non cambia.
Se lo stai usando solo internamente, potresti non preoccuparti così tanto e l'utilizzo List<T>
potrebbe essere ok.
La risposta meno popolare è che ai programmatori piace far finta che il loro software verrà riutilizzato in tutto il mondo, quando in realtà la maggior parte dei progetti sarà gestita da una piccola quantità di persone e per quanto i morsi sonori relativi all'interfaccia siano belli, stai illudendo te stesso.
Astronauti di architettura . Le possibilità che tu abbia mai scritto la tua IList che aggiunge qualcosa a quelle già presenti nel framework .NET sono così remote che si tratta di jelly tot teorici riservati alle "migliori pratiche".
Ovviamente se ti viene chiesto quale usi durante un'intervista, dici IList, sorridi ed entrambi ti compiaci per essere così intelligente. O per un'API pubblica, IList. Spero che tu ottenga il mio punto.
IEnumerable<string>
, all'interno della funzione potrei usare a List<string>
per un archivio di supporto interno per generare la mia raccolta, ma voglio solo che i chiamanti ne enumerino il contenuto, non aggiungano o rimuovano. Accettare un'interfaccia come parametro comunica un messaggio simile "Ho bisogno di un insieme di stringhe, non ti preoccupare, non lo cambierò."
L'interfaccia è una promessa (o un contratto).
Come sempre con le promesse, più piccolo è, meglio è .
IList
è la promessa. Qual è la promessa più piccola e migliore qui? Questo non è logico.
List<T>
, perché AddRange
non è definito sull'interfaccia.
IList<T>
fa promesse che non può mantenere. Ad esempio, esiste un Add
metodo che può essere lanciato se IList<T>
sembra essere di sola lettura. List<T>
d'altra parte è sempre scrivibile e quindi mantiene sempre le sue promesse. Inoltre, da .NET 4.6, ora abbiamo IReadOnlyCollection<T>
e IReadOnlyList<T>
che sono quasi sempre più adatti di IList<T>
. E sono covarianti. Evitare IList<T>
!
IList<T>
. Qualcuno potrebbe anche implementare IReadOnlyList<T>
e rendere ogni metodo anche un'eccezione. È un po 'l'opposto della dattilografia: l'hai chiamata anatra, ma non può ciarlare come una. Fa parte del contratto, anche se il compilatore non può applicarlo. Concordo al 100% sul fatto che, IReadOnlyList<T>
o comunque, IReadOnlyCollection<T>
debba essere utilizzato per l'accesso in sola lettura.
Alcune persone dicono "usa sempre IList<T>
invece di List<T>
".
Vogliono che cambi le firme del tuo metodo da void Foo(List<T> input)
a void Foo(IList<T> input)
.
Queste persone hanno torto.
È più sfumato di così. Se stai restituendo IList<T>
un'interfaccia pubblica alla tua libreria, ti lasci interessanti opzioni per magari creare un elenco personalizzato in futuro. Potresti non aver mai bisogno di quell'opzione, ma è un argomento. Penso che sia l'intero argomento per restituire l'interfaccia invece del tipo concreto. Vale la pena menzionarlo, ma in questo caso ha un grave difetto.
Come oggetto secondario secondario, potresti trovare che ogni singolo chiamante necessita List<T>
comunque di un codice e il codice chiamante è disseminato di.ToList()
Ma cosa ancora più importante, se si accetta un IList come parametro, è meglio fare attenzione, perché IList<T>
e List<T>
non si comportano allo stesso modo. Nonostante la somiglianza nel nome e nonostante condividano un'interfaccia , non espongono lo stesso contratto .
Supponiamo di avere questo metodo:
public Foo(List<int> a)
{
a.Add(someNumber);
}
Un collega utile "refactor" il metodo da accettare IList<int>
.
Il tuo codice ora è rotto, perché int[]
implementa IList<int>
, ma ha dimensioni fisse. Il contratto per ICollection<T>
(la base di IList<T>
) richiede il codice che lo utilizza per controllare la IsReadOnly
bandiera prima di tentare di aggiungere o rimuovere elementi dalla raccolta. Il contratto per List<T>
no.
Il principio di sostituzione di Liskov (semplificato) afferma che un tipo derivato dovrebbe essere in grado di essere utilizzato al posto di un tipo base, senza precondizioni o postcondizioni aggiuntive.
Questo sembra infrangere il principio di sostituzione di Liskov.
int[] array = new[] {1, 2, 3};
IList<int> ilist = array;
ilist.Add(4); // throws System.NotSupportedException
ilist.Insert(0, 0); // throws System.NotSupportedException
ilist.Remove(3); // throws System.NotSupportedException
ilist.RemoveAt(0); // throws System.NotSupportedException
Ma non lo fa. La risposta a questo è che l'esempio ha usato IList <T> / ICollection <T> errato. Se usi un ICollection <T> devi controllare il flag IsReadOnly.
if (!ilist.IsReadOnly)
{
ilist.Add(4);
ilist.Insert(0, 0);
ilist.Remove(3);
ilist.RemoveAt(0);
}
else
{
// what were you planning to do if you were given a read only list anyway?
}
Se qualcuno ti passa un array o un elenco, il tuo codice funzionerà bene se controlli la bandiera ogni volta e hai un fallback ... Ma davvero; chi lo fa? Non sai in anticipo se il tuo metodo necessita di un elenco che può richiedere membri aggiuntivi; non lo specifichi nella firma del metodo? Cosa avresti fatto esattamente se ti fosse passata una lista di sola lettura come int[]
?
È possibile sostituire un List<T>
nel codice che utilizza IList<T>
/ ICollection<T>
correttamente . Non è possibile garantire che è possibile sostituire un IList<T>
/ ICollection<T>
nel codice che utilizza List<T>
.
C'è un appello al principio di responsabilità singola / principio di segregazione dell'interfaccia in molti degli argomenti per usare le astrazioni anziché i tipi concreti - dipende dall'interfaccia più stretta possibile. Nella maggior parte dei casi, se stai utilizzando un List<T>
e pensi di poter invece utilizzare un'interfaccia più stretta - perché no IEnumerable<T>
? Questa è spesso una soluzione migliore se non è necessario aggiungere elementi. Se è necessario aggiungere alla collezione, utilizzare il tipo di cemento, List<T>
.
Per me IList<T>
(e ICollection<T>
) è la parte peggiore del framework .NET. IsReadOnly
viola il principio della minima sorpresa. Una classe come, ad esempio Array
, che non consente mai di aggiungere, inserire o rimuovere elementi non deve implementare un'interfaccia con i metodi Aggiungi, Inserisci e Rimuovi. (vedi anche /software/306105/implementing-an-interface-when-you-dont-need-one-of-the-properties )
È IList<T>
adatto alla tua organizzazione? Se un collega ti chiede di modificare una firma del metodo da utilizzare IList<T>
invece di List<T>
, chiedi loro come aggiungerebbero un elemento a un IList<T>
. Se non conoscono IsReadOnly
(e la maggior parte delle persone non lo sa), allora non usare IList<T>
. Mai.
Si noti che il flag IsReadOnly proviene da ICollection <T> e indica se gli elementi possono essere aggiunti o rimossi dalla raccolta; ma solo per confondere davvero le cose, non indica se possono essere sostituite, cosa che nel caso degli array (che restituiscono IsReadOnlys == true) può essere.
Per ulteriori informazioni su IsReadOnly, vedere la definizione msdn di ICollection <T> .IsReadOnly
IsReadOnly
è true
per un IList
, puoi ancora modificare i suoi membri attuali! Forse quella proprietà dovrebbe essere nominata IsFixedSize
?
Array
come IList
, quindi è per questo che ho potuto modificare i membri attuali)
IReadOnlyCollection<T>
e IReadOnlyList<T>
che sono quasi sempre meglio adattati rispetto a quelli IList<T>
ma non hanno la pericolosa semantica pigra IEnumerable<T>
. E sono covarianti. Evitare IList<T>
!
List<T>
è un'implementazione specifica di IList<T>
, che è un contenitore che può essere indirizzato allo stesso modo di un array lineare T[]
usando un indice intero. Quando si specifica IList<T>
come tipo dell'argomento del metodo, si specifica solo che sono necessarie determinate funzionalità del contenitore.
Ad esempio, la specifica dell'interfaccia non impone una struttura di dati specifica da utilizzare. L'implementazione di List<T>
accade alle stesse prestazioni per l'accesso, l'eliminazione e l'aggiunta di elementi come un array lineare. Tuttavia, è possibile immaginare un'implementazione supportata da un elenco collegato, per il quale l'aggiunta di elementi alla fine è più economica (tempo costante) ma l'accesso casuale molto più costoso. (Si noti che il .NET LinkedList<T>
non non implementa IList<T>
.)
Questo esempio indica inoltre che potrebbero essere presenti situazioni in cui è necessario specificare l'implementazione, non l'interfaccia, nell'elenco degli argomenti: In questo esempio, ogni volta che si richiede una caratteristica di prestazione di accesso particolare. Questo di solito è garantito per un'implementazione specifica di un contenitore ( List<T>
documentazione: "Implementa l' IList<T>
interfaccia generica usando un array le cui dimensioni sono aumentate dinamicamente come richiesto.").
Inoltre, potresti prendere in considerazione l'idea di esporre la minima funzionalità di cui hai bisogno. Per esempio. se non è necessario modificare il contenuto dell'elenco, è consigliabile considerare l'utilizzo IEnumerable<T>
, che si IList<T>
estende.
LinkedList<T>
vs prendendo List<T>
vs IList<T>
tutti comunicano qualcosa delle garanzie prestazionali richieste dal codice chiamato.
Vorrei cambiare leggermente la domanda, invece di giustificare il motivo per cui dovresti usare l'interfaccia sull'implementazione concreta, proverei a giustificare il motivo per cui dovresti usare l'implementazione concreta piuttosto che l'interfaccia. Se non puoi giustificarlo, usa l'interfaccia.
IList <T> è un'interfaccia in modo da poter ereditare un'altra classe e comunque implementare IList <T> mentre ereditare List <T> ti impedisce di farlo.
Ad esempio, se esiste una classe A e la tua classe B la eredita, non è possibile utilizzare Elenco <T>
class A : B, IList<T> { ... }
Un principio di TDD e OOP è in genere la programmazione di un'interfaccia e non di un'implementazione.
In questo caso specifico dal momento che stai essenzialmente parlando di un costrutto linguistico, non di un linguaggio personalizzato generalmente non importerà, ma per esempio, ad esempio, hai trovato che List non supportava qualcosa di cui avevi bisogno. Se avessi usato IList nel resto dell'app, avresti potuto estendere List con la tua classe personalizzata ed essere comunque in grado di farlo passare senza refactoring.
Il costo per farlo è minimo, perché non risparmiarti il mal di testa in seguito? È il principio dell'interfaccia.
public void Foo(IList<Bar> list)
{
// Do Something with the list here.
}
In questo caso è possibile passare in qualsiasi classe che implementa l'interfaccia IList <Bar>. Se invece hai utilizzato Elenco <Bar>, è possibile passare solo un'istanza Elenco <Bar>.
Il modo IList <Bar> è accoppiato più liberamente rispetto al modo List <Bar>.
Supporre che nessuna di queste domande (o risposte) dell'elenco contro IList menzioni la differenza di firma. (Ecco perché ho cercato questa domanda su SO!)
Quindi ecco i metodi contenuti in List che non si trovano in IList, almeno a partire da .NET 4.5 (circa 2015)
Reverse
e ToArray
sono metodi di estensione per l'interfaccia IEnumerable <T>, da cui IList <T> deriva.
List
non in altre implementazioni di IList
.
Il caso più importante per l'utilizzo delle interfacce rispetto alle implementazioni è nei parametri dell'API. Se la tua API accetta un parametro Elenco, chiunque lo usi deve usare Elenco. Se il tipo di parametro è IList, il chiamante ha molta più libertà e può usare classi di cui non hai mai sentito parlare, che potrebbero non esistere nemmeno al momento della scrittura del codice.
Che cosa succede se .NET 5.0 sostituisce System.Collections.Generic.List<T>
a System.Collection.Generics.LinearList<T>
. .NET possiede sempre il nome List<T>
ma garantisce che si IList<T>
tratta di un contratto. Quindi IMHO noi (almeno I) non dovremmo usare il nome di qualcuno (anche se in questo caso è .NET) e metterci nei guai in seguito.
In caso di utilizzo IList<T>
, il chiamante è sempre in regola per far funzionare le cose e l'implementatore è libero di modificare la raccolta sottostante in qualsiasi implementazione alternativa concreta diIList
Tutti i concetti sono fondamentalmente indicati nella maggior parte delle risposte sopra riguardanti il motivo per cui utilizzare l'interfaccia su implementazioni concrete.
IList<T> defines those methods (not including extension methods)
IList<T>
Collegamento MSDN
List<T>
implementa questi nove metodi (esclusi i metodi di estensione), oltre a disporre di circa 41 metodi pubblici, che pesa in considerazione di quale utilizzare nella propria applicazione.
List<T>
Collegamento MSDN
Lo faresti perché la definizione di un IList o di un ICollection si aprirà per altre implementazioni delle tue interfacce.
Potresti voler avere un IOrderRepository che definisce una raccolta di ordini in un IList o ICollection. Potresti quindi avere diversi tipi di implementazioni per fornire un elenco di ordini purché conformi alle "regole" definite dal tuo IList o ICollection.
IList <> è quasi sempre preferibile secondo i consigli dell'altro poster, tuttavia nota che c'è un bug in .NET 3.5 sp 1 quando si esegue un IList <> attraverso più di un ciclo di serializzazione / deserializzazione con WCF DataContractSerializer.
Esiste ora un SP per correggere questo errore: KB 971030
L'interfaccia ti assicura almeno di ottenere i metodi che ti aspetti ; essere consapevoli della definizione dell'interfaccia cioè. tutti i metodi astratti che sono lì per essere implementati da qualsiasi classe che eredita l'interfaccia. quindi se qualcuno crea una sua enorme classe con diversi metodi oltre a quelli che ha ereditato dall'interfaccia per alcune funzionalità aggiuntive e quelli che non ti sono utili, è meglio usare un riferimento a una sottoclasse (in questo caso il interfaccia) e assegnare l'oggetto classe concreto ad esso.
ulteriore vantaggio è che il tuo codice è al sicuro da qualsiasi modifica alla classe concreta poiché stai sottoscrivendo solo alcuni dei metodi della classe concreta e quelli sono quelli che rimarranno lì fintanto che la classe concreta eredita dall'interfaccia che sei utilizzando. quindi la sua sicurezza per te e la libertà per il programmatore che sta scrivendo un'implementazione concreta per cambiare o aggiungere più funzionalità alla sua classe concreta.
Puoi guardare questo argomento da diversi punti di vista, incluso quello di un approccio puramente OO che dice di programmare contro un'interfaccia e non un'implementazione. Con questo pensiero, l'utilizzo di IList segue lo stesso principio del passaggio e dell'utilizzo delle interfacce definite dall'utente. Credo anche nei fattori di scalabilità e flessibilità forniti da un'interfaccia in generale. Se una classe che impianta IList <T> deve essere estesa o modificata, il codice di consumo non deve cambiare; sa a cosa aderisce il contratto di IList Interface. Tuttavia, l'utilizzo di un'implementazione concreta e dell'elenco <T> su una classe che cambia potrebbe comportare la modifica del codice chiamante. Questo perché una classe che aderisce a IList <T> garantisce un determinato comportamento che non è garantito da un tipo concreto usando List <T>.
Avere anche il potere di fare qualcosa come modificare l'implementazione predefinita di List <T> su una classe Implementare IList <T> per esempio .Add, .Remove o qualsiasi altro metodo IList offre allo sviluppatore molta flessibilità e potenza, altrimenti predefinite per Elenco <T>
In genere, un buon approccio consiste nell'utilizzare IList nell'API di fronte al pubblico (quando appropriato e sono necessari elenchi semantici), quindi Elenca internamente per implementare l'API. Ciò consente di passare a un'implementazione diversa di IList senza interrompere il codice che utilizza la classe.
L'elenco dei nomi di classe può essere modificato nel prossimo framework .net ma l'interfaccia non cambierà mai poiché l'interfaccia è un contratto.
Nota che, se la tua API verrà utilizzata solo in cicli foreach, ecc., Potresti prendere in considerazione l'idea di esporre IEnumerable.