IEnumerable come tipo di ritorno

C’è un problema con l’utilizzo di IEnumerable come tipo di ritorno? FxCop si lamenta della restituzione di List (si consiglia invece di restituire Collection ).

Beh, sono sempre stato guidato da una regola “accetta il minimo che puoi, ma restituisci il massimo.”

Da questo punto di vista, restituire IEnumerable è una cosa negativa, ma cosa dovrei fare quando voglio utilizzare “lazy retrieval”? Inoltre, la parola chiave yield è un vero tesoro.

Questa è davvero una domanda in due parti.

1) C’è intrinsecamente qualcosa di sbagliato nel restituire un object IEnumerable

Niente di niente. Infatti se si utilizzano gli iteratori C # questo è il comportamento previsto. Convertirlo in un elenco o in un’altra class di raccolta preventivamente non è una buona idea. Fare così è un’ipotesi sul modello di utilizzo da parte del chiamante. Trovo che non sia una buona idea assumere qualcosa sul chiamante. Possono avere buoni motivi per cui desiderano un IEnumerable . Forse vogliono convertirlo in una gerarchia di raccolta completamente diversa (nel qual caso viene sprecata una conversione in List).

2) Ci sono circostanze in cui potrebbe essere preferibile restituire qualcosa di diverso da IEnumerable ?

Sì. Anche se non è una buona idea dare troppo per scontato i tuoi chiamanti, è perfettamente accettabile prendere decisioni basate sul tuo comportamento. Immagina uno scenario in cui avevi un object multi-thread che stava accodando le richieste a un object che veniva costantemente aggiornato. In questo caso restituire un IEnumerable non valido è irresponsabile. Non appena la raccolta viene modificata, l’enumerazione viene invalidata e causerà l’esecuzione di un evento. Invece potresti fare uno snapshot della struttura e restituire quel valore. Dì in un modulo . In questo caso vorrei solo restituire l’object come struttura diretta (o interfaccia).

Questo è certamente il caso più raro però.

No, IEnumerable è una buona cosa per tornare qui, poiché tutto ciò che prometti è “una sequenza di valori (tipizzati)”. Ideale per LINQ ecc. E perfettamente utilizzabile.

Il chiamante può facilmente mettere questi dati in una lista (o qualsiasi altra cosa) – specialmente con LINQ ( ToList , ToArray , ecc.).

Questo approccio consente di eseguire lo spooling dei valori, piuttosto che doverli bufferizzare tutti i dati. Sicuramente un tesoro. Ho scritto un altro utile trucco IEnumerable l’altro giorno.

IEnumerable va bene per me ma ha alcuni inconvenienti. Il cliente deve enumerare per ottenere i risultati. Non ha modo di controllare il conteggio ecc. L’elenco è negativo perché esponi a un controllo eccessivo; il client può aggiungere / rimuovere ecc. e ciò può essere una cosa negativa. La collezione sembra il miglior compromesso, almeno nella visione di FxCop. Uso sempre ciò che sembra appropriato nel mio contesto (per esempio se voglio restituire una raccolta di sola lettura espongo la raccolta come tipo di ritorno e restituisco List.AsReadOnly () o IEnumerable per la valutazione lazy tramite yield ecc.). Prendilo caso per caso

Circa il tuo principio: “accetta il minimo che puoi, ma restituisci il massimo”.

La chiave per gestire la complessità di un grande programma è una tecnica chiamata occultamento dell’informazione . Se il tuo metodo funziona costruendo un List , non è spesso necessario rivelare questo fatto restituendo quel tipo. Se lo fai, allora i chiamanti possono modificare l’elenco che ottengono. Ciò rimuove la capacità di fare il caching o l’iterazione pigra con yield return .

Quindi un principio migliore è che una funzione da seguire è: “rivelare il meno ansible su come si lavora”.

“accetta il minimo che puoi, ma restituisci il massimo” è ciò che sostengo. Quando un metodo restituisce un object, quali giustificazioni non dobbiamo restituire il tipo effettivo e limitare le capacità dell’object restituendo un tipo di base. Ciò solleva tuttavia una domanda: come possiamo sapere quale sarà il “massimo” (tipo effettivo) quando progettiamo un’interfaccia. La risposta è molto semplice. Solo nei casi estremi in cui il progettista dell’interfaccia sta progettando un’interfaccia aperta, che verrà implementata al di fuori dell’applicazione / componente, non saprebbe quale potrebbe essere il tipo di reso effettivo. Un progettista intelligente dovrebbe sempre considerare cosa dovrebbe fare il metodo e quale dovrebbe essere un tipo di ritorno ottimale / generico.

Ad esempio, se sto progettando un’interfaccia per recuperare un vettore di oggetti e so che il conteggio degli oggetti restituiti sarà variabile, presumo sempre che uno sviluppatore intelligente userà sempre un elenco. Se qualcuno prevede di restituire una matrice, metterei in discussione le sue capacità, a meno che non stia semplicemente restituendo i dati da un altro strato che non possiede. E questo è probabilmente il motivo per cui FxCop sostiene ICollection (base comune per List e Array).

Quanto sopra detto, ci sono un paio di altre cose da considerare

  • se i dati restituiti devono essere mutevoli o immutabili

  • se i dati restituiti sono condivisi tra più chiamanti

Per quanto riguarda le valutazioni pigre LINQ, sono sicuro che gli utenti del 95% + C # non capiscono l’intestino. È così non-oo-ish. OO promuove cambiamenti concreti dello stato sulle invocazioni dei metodi. La valutazione lenta LINQ promuove le modifiche dello stato di runtime sul modello di valutazione dell’espressione (non sempre gli utenti non avanzati seguono sempre).

Restituire IEnumerable è OK se stai solo restituendo una enumerazione, e sarà consumata dal tuo chiamante in quanto tale.

Ma come altri sottolineano, ha lo svantaggio che il chiamante potrebbe dover enumerare se ha bisogno di altre informazioni (ad esempio Conteggio). Il metodo di estensione .NET 3.5 IEnumerable .Count enumererà dietro le quinte se il valore restituito non implementa ICollection , che potrebbe essere indesiderabile.

Spesso restituisco IList o ICollection quando il risultato è una collezione – internamente il tuo metodo può usare un List e restituirlo così com’è, o restituire List .AsReadOnly se vuoi proteggere contro la modifica (ad esempio se si memorizza la lista internamente nella cache). AFAIK FxCop è abbastanza soddisfatto di entrambi.

Un aspetto importante è che quando si restituisce un List si sta effettivamente restituendo un riferimento . Ciò rende ansible che un chiamante manipoli la tua lista. Questo è un problema comune, ad esempio un livello aziendale che restituisce un List a un livello della GUI.

Solo perché dici che stai restituendo IEnumerable non significa che non puoi restituire una lista. L’idea è di ridurre l’accoppiamento non necessario. Tutto ciò di cui il chiamante dovrebbe preoccuparsi è ottenere un elenco di cose, piuttosto che il tipo esatto di raccolta utilizzata per contenere quell’elenco. Se hai qualcosa che è supportato da un array, quindi ottenere qualcosa come Count sarà comunque veloce.

Penso che la tua guida sia ottima – se sei in grado di essere più specifico su ciò che stai restituendo senza un risultato in termini di prestazioni (non devi, ad esempio, build un elenco con i tuoi risultati), fallo. Ma se la tua funzione legittima non sa che tipo troverà, come se in alcune situazioni tu lavorassi con una lista e in alcuni con una matrice, ecc., Quindi restituire IEnumerable è il “meglio” che puoi fare . Consideralo come il “più grande multiplo comune” di tutto ciò che potresti voler tornare.

Non posso accettare la risposta scelta. Ci sono modi per affrontare lo scenario descritto ma usando una Lista o qualsiasi altra cosa il tuo utilizzo non è uno di questi. Nel momento in cui viene restituito l’object IEnumerable è necessario assumere che il chiamante potrebbe eseguire un foreach. In tal caso, non importa se il tipo concreto è List o spaghetti. In effetti solo l’indicizzazione è un problema soprattutto se gli articoli vengono rimossi.

Qualsiasi valore restituito è un’istantanea. Può essere il contenuto corrente dell’IEnumerable, nel qual caso se è memorizzato nella cache dovrebbe essere un clone della copia cache; se si suppone che sia più dinamico (come i resuts di una query sql) usa il rendimento yield; tuttavia permettendo al contenitore di mutare a piacimento e fornire metodi come Count e indexer è una ricetta per il disastro in un mondo multithreaded. Non sono nemmeno riuscito a far chiamare il chiamante Aggiungi o Elimina su un contenitore che il tuo codice dovrebbe avere sotto controllo.

Anche la restituzione di un tipo concreto ti blocca in un’implementazione. Oggi internamente potresti usare una lista. Domani forse diventi multithread e vuoi usare un contenitore thread safe o un array o una coda o la raccolta Values ​​di un dizionario o l’output di una query Linq. Se ti blocchi in un tipo di reso concreto, devi modificare un gruppo di codice o effettuare una conversione prima di tornare.