Gestione degli avvisi per possibile enumerazione multipla di IEnumerable


359

Nel mio codice ho bisogno di utilizzare IEnumerable<>più volte, quindi ottenere l'errore Resharper di "Possibile enumerazione multipla di IEnumerable".

Codice di esempio:

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null || !objects.Any())
        throw new ArgumentException();

    var firstObject = objects.First();
    var list = DoSomeThing(firstObject);        
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}
  • Posso modificare il objectsparametro in modo da Listevitare quindi la possibile enumerazione multipla ma non ottengo l'oggetto più alto che posso gestire.
  • Un'altra cosa che posso fare è quello di convertire il IEnumerableal Listall'inizio del metodo:

 public List<object> Foo(IEnumerable<object> objects)
 {
    var objectList = objects.ToList();
    // ...
 }

Ma questo è solo imbarazzante .

Cosa faresti in questo scenario?

Risposte:


471

Il problema con prendere IEnumerablecome parametro è che dice ai chiamanti "Vorrei enumerarlo". Non dice loro quante volte desideri enumerare.

Posso modificare il parametro objects in List e quindi evitare la possibile enumerazione multipla ma non ottengo l'oggetto più alto che posso gestire .

L'obiettivo di prendere l'oggetto più alto è nobile, ma lascia spazio a troppe ipotesi. Vuoi davvero che qualcuno passi una query LINQ to SQL a questo metodo, solo per te enumerarlo due volte (ottenendo risultati potenzialmente diversi ogni volta?)

La mancanza semantica qui è che un chiamante, che forse non impiega il tempo a leggere i dettagli del metodo, può presumere che tu esegua l'iterazione una sola volta, quindi ti passa un oggetto costoso. La firma del metodo non indica in entrambi i casi.

Modificando la firma del metodo su IList/ ICollection, almeno renderai più chiaro al chiamante quali sono le tue aspettative e possono evitare costosi errori.

Altrimenti, la maggior parte degli sviluppatori che esaminano il metodo potrebbe presumere che tu esegua l'iterazione una sola volta. Se prendere un IEnumerableè così importante, dovresti considerare di farlo .ToList()all'inizio del metodo.

È un peccato che .NET non abbia un'interfaccia IEnumerable + Count + Indexer, senza i metodi Aggiungi / Rimuovi ecc., Che sospetto risolverebbe questo problema.


32
ReadOnlyCollection <T> soddisfa i requisiti dell'interfaccia desiderati? msdn.microsoft.com/en-us/library/ms132474.aspx
Dan Fiddling By Firelight

74
@DanNeely Suggerirei IReadOnlyCollection(T)(nuovo con .net 4.5) come la migliore interfaccia per trasmettere l'idea che si tratti di un oggetto IEnumerable(T)che deve essere elencato più volte. Come afferma questa risposta, IEnumerable(T)è di per sé così generico che può persino riferirsi a contenuti non ripristinabili che non possono essere nuovamente enumerati senza effetti collaterali. Ma IReadOnlyCollection(T)implica una reiterazione.
binki,

1
Se lo fai .ToList()all'inizio del metodo, devi prima verificare se il parametro non è null. Ciò significa che otterrai due punti in cui si genera ArgumentException: se il parametro è null e se non ha elementi.
comecme

Ho letto questo articolo sulla semantica di IReadOnlyCollection<T>vs IEnumerable<T>e poi ho posto una domanda sull'uso IReadOnlyCollection<T>come parametro e in quali casi. Penso che la conclusione a cui sono arrivato alla fine sia la logica da seguire. Che dovrebbe essere usato dove è prevista l'enumerazione, non necessariamente dove potrebbe esserci un'enumerazione multipla.
Neo

32

Se i tuoi dati saranno sempre ripetibili, forse non ti preoccupare. Tuttavia, puoi anche srotolarlo - questo è particolarmente utile se i dati in arrivo potrebbero essere di grandi dimensioni (ad esempio, lettura da disco / rete):

if(objects == null) throw new ArgumentException();
using(var iter = objects.GetEnumerator()) {
    if(!iter.MoveNext()) throw new ArgumentException();

    var firstObject = iter.Current;
    var list = DoSomeThing(firstObject);  

    while(iter.MoveNext()) {
        list.Add(DoSomeThingElse(iter.Current));
    }
    return list;
}

Nota: ho modificato un po 'la semantica di DoSomethingElse, ma principalmente per mostrare l'utilizzo non srotolato. Ad esempio, potresti ricartare nuovamente l'iteratore. Potresti renderlo anche un blocco iteratore, il che potrebbe essere carino; allora non c'è list- e tu vorresti yield returngli articoli come li ottieni, piuttosto che aggiungere a un elenco da restituire.


4
+1 Beh, questo è un codice molto carino, ma ... Perdo la bellezza e la leggibilità del codice.
gdoron sostiene Monica il

5
@gdoron non tutto è carino; p non quanto sopra è illeggibile, però. Soprattutto se è trasformato in un blocco iteratore - piuttosto carino, IMO.
Marc Gravell

Posso solo scrivere: "var objectList = objects.ToList ();" e salva tutta la gestione dell'enumeratore.
gdoron sostiene Monica il

10
@gdoron nella domanda che hai esplicitamente detto che non volevi davvero farlo; p Inoltre, un approccio ToList () potrebbe non adattarsi se i dati se sequenze molto grandi (potenzialmente infinite) non devono essere finiti, ma possono essere ancora iterato). Alla fine, sto solo presentando un'opzione. Solo tu conosci l'intero contesto per vedere se è garantito. Se i tuoi dati sono ripetibili, un'altra opzione valida è: non modificare nulla! Ignora R # invece ... tutto dipende dal contesto.
Marc Gravell

1
Penso che la soluzione di Marc sia piuttosto carina. 1. È molto chiaro come viene inizializzato l '"elenco", è chiaro come viene iterato l'elenco ed è chiaro su quali membri dell'elenco vengono gestiti.
Keith Hoffman,

9

L'utilizzo IReadOnlyCollection<T>o IReadOnlyList<T>nella firma del metodo anziché IEnumerable<T>, ha il vantaggio di rendere esplicito che potrebbe essere necessario controllare il conteggio prima di iterare o iterare più volte per qualche altro motivo.

Tuttavia, hanno un enorme svantaggio che causerà problemi se si tenta di refactoring del codice per utilizzare le interfacce, ad esempio per renderlo più testabile e intuitivo per il proxy dinamico. Il punto chiave è che IList<T>non ereditaIReadOnlyList<T> e allo stesso modo per altre raccolte e le rispettive interfacce di sola lettura. (In breve, ciò è dovuto al fatto che .NET 4.5 voleva mantenere la compatibilità ABI con le versioni precedenti. Ma non ha nemmeno colto l'occasione per modificarlo in .NET core. )

Ciò significa che se si ottiene un IList<T>da una parte del programma e si desidera passarlo a un'altra parte che si aspetta un IReadOnlyList<T>, non è possibile! Puoi comunque passare un IList<T>come unIEnumerable<T> .

Alla fine, IEnumerable<T>è l'unica interfaccia di sola lettura supportata da tutte le raccolte .NET, comprese tutte le interfacce di raccolta. Qualsiasi altra alternativa tornerà a morderti mentre ti rendi conto di esserti bloccato da alcune scelte di architettura. Quindi penso che sia il tipo giusto da usare nelle firme di funzione per esprimere che vuoi solo una raccolta di sola lettura.

(Nota che puoi sempre scrivere un IReadOnlyList<T> ToReadOnly<T>(this IList<T> list)metodo di estensione che lanci semplicemente se il tipo sottostante supporta entrambe le interfacce, ma devi aggiungerlo manualmente ovunque durante il refactoring, dove IEnumerable<T>è sempre compatibile.)

Come sempre questo non è un assoluto, se stai scrivendo un codice pesante di database in cui l'enumerazione multipla accidentale sarebbe un disastro, potresti preferire un altro compromesso.


2
+1 Per essere arrivato su un vecchio post e aggiungere documentazione su nuovi modi per risolvere questo problema con le nuove funzionalità fornite dalla piattaforma .NET (ovvero IReadOnlyCollection <T>)
Kevin Avignon,

4

Se l'obiettivo è davvero quello di prevenire più enumerazioni rispetto alla risposta di Marc Gravell è quella da leggere, ma mantenendo la stessa semantica si potrebbe semplicemente rimuovere il ridondante Anye le Firstchiamate e procedere con:

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null)
        throw new ArgumentNullException("objects");

    var first = objects.FirstOrDefault();

    if (first == null)
        throw new ArgumentException(
            "Empty enumerable not supported.", 
            "objects");

    var list = DoSomeThing(first);  

    var secondList = DoSomeThingElse(objects);

    list.AddRange(secondList);

    return list;
}

Tieni presente che ciò presuppone che tu IEnumerablenon sia generico o che almeno sia vincolato a un tipo di riferimento.


2
objectsviene ancora passato a DoSomethingElse che restituisce un elenco, quindi è ancora presente la possibilità che si stiano ancora enumerando due volte. In entrambi i casi, se la raccolta era una query LINQ to SQL, si colpisce il DB due volte.
Paul Stovell,

1
@PaulStovell, Leggi questo: "Se lo scopo è davvero quello di prevenire più enumerazioni rispetto alla risposta di Marc Gravell è quella da leggere" =)
gdoron supporta Monica il

È stato suggerito su alcuni altri post di stackoverflow sul lancio di eccezioni per elenchi vuoti e lo suggerirò qui: perché gettare un'eccezione in un elenco vuoto? Ottieni un elenco vuoto, fornisci un elenco vuoto. Come paradigma, è piuttosto semplice. Se il chiamante non sa che sta passando un elenco vuoto, c'è un problema.
Keith Hoffman,

@Keith Hoffman, il codice di esempio mantiene lo stesso comportamento del codice pubblicato dall'OP, in cui l'uso Firstgenererà un'eccezione per un elenco vuoto. Non credo sia possibile dire di non gettare mai eccezioni nell'elenco vuoto o di lanciare sempre eccezioni nell'elenco vuoto. Dipende ...
João Angelo

Abbastanza giusto Joao. Più spunti di riflessione che critiche al tuo post.
Keith Hoffman,

3

Di solito sovraccarico il mio metodo con IEnumerable e IList in questa situazione.

public static IEnumerable<T> Method<T>( this IList<T> source ){... }

public static IEnumerable<T> Method<T>( this IEnumerable<T> source )
{
    /*input checks on source parameter here*/
    return Method( source.ToList() );
}

Mi occupo di spiegare nei commenti di sintesi dei metodi che la chiamata a IEnumerable eseguirà un .ToList ().

Il programmatore può scegliere .ToList () a un livello superiore se vengono concatenate più operazioni e quindi chiamare il sovraccarico IList o lasciare che il mio sovraccarico IEnumerable si occupi di ciò.


20
Questo è orribile.
Mike Chamberlain,

1
@MikeChamberlain Sì, è davvero orribile e assolutamente pulito allo stesso tempo. Purtroppo alcuni scenari pesanti hanno bisogno di questo approccio.
Mauro Sampietro,

1
Ma poi ricreare l'array ogni volta che lo si passa al metodo parameratizzato IEnumerable. Sono d'accordo con @MikeChamberlain a questo proposito, questo è un suggerimento terribile.
Krythic,

2
@Krythic Se non è necessario ricreare l'elenco, eseguire un ToList da qualche altra parte e utilizzare il sovraccarico IList. Questo è il punto di questa soluzione.
Mauro Sampietro,

0

Se devi solo controllare il primo elemento, puoi sbirciarlo senza iterare l'intera raccolta:

public List<object> Foo(IEnumerable<object> objects)
{
    object firstObject;
    if (objects == null || !TryPeek(ref objects, out firstObject))
        throw new ArgumentException();

    var list = DoSomeThing(firstObject);
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}

public static bool TryPeek<T>(ref IEnumerable<T> source, out T first)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    IEnumerator<T> enumerator = source.GetEnumerator();
    if (!enumerator.MoveNext())
    {
        first = default(T);
        source = Enumerable.Empty<T>();
        return false;
    }

    first = enumerator.Current;
    T firstElement = first;
    source = Iterate();
    return true;

    IEnumerable<T> Iterate()
    {
        yield return firstElement;
        using (enumerator)
        {
            while (enumerator.MoveNext())
            {
                yield return enumerator.Current;
            }
        }
    }
}

-3

Prima di tutto, quell'avvertimento non significa sempre molto. Di solito lo disabilitavo dopo essermi assicurato che non fosse un collo di bottiglia da performance. Significa solo che IEnumerableviene valutato due volte, che di solito non è un problema a meno che lo evaluationstesso non impieghi molto tempo. Anche se ci vuole molto tempo, in questo caso stai usando un solo elemento la prima volta.

In questo scenario potresti anche sfruttare ancora di più i potenti metodi di estensione linq.

var firstObject = objects.First();
return DoSomeThing(firstObject).Concat(DoSomeThingElse(objects).ToList();

In IEnumerablequesto caso è possibile valutare solo una volta con qualche seccatura, ma prima profilare e vedere se è davvero un problema.


15
Lavoro molto con IQueryable e disattivare questo tipo di avvisi è molto pericoloso.
gdoron sostiene Monica il

5
Valutare due volte è una cosa negativa nei miei libri. Se il metodo accetta IEnumerable, potrei essere tentato di passargli una query LINQ to SQL, che si traduce in più chiamate DB. Poiché la firma del metodo non indica che può essere enumerata due volte, dovrebbe cambiare la firma (accettare IList / ICollection) o iterare una sola volta
Paul Stovell,

4
l'ottimizzazione precoce e la rovina della leggibilità e quindi la possibilità di fare ottimizzazioni che fanno una differenza in seguito è molto peggio nel mio libro.
vidstige,

Quando hai una query di database, assicurarti che sia valutata solo una volta che fa già la differenza. Non è solo un vantaggio teorico futuro, è un vero beneficio misurabile in questo momento. Non solo, valutare una sola volta non è solo benefico per le prestazioni, ma è anche necessario per la correttezza. L'esecuzione di una query del database due volte può produrre risultati diversi, in quanto qualcuno potrebbe aver emesso un comando di aggiornamento tra le due valutazioni della query.

1
@hvd Non ho raccomandato nulla del genere. Ovviamente non dovresti elencare IEnumerablesciò che dà risultati diversi ogni volta che lo iteri o che impiega molto tempo per iterare. Vedi la risposta di Marc Gravells - Anche lui afferma innanzitutto "forse non ti preoccupare". Il fatto è che molte volte lo vedi non hai nulla di cui preoccuparti.
vidstige,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.