Perché List <T> .ForEach consente la modifica della sua lista?


90

Se uso:

var strings = new List<string> { "sample" };
foreach (string s in strings)
{
  Console.WriteLine(s);
  strings.Add(s + "!");
}

l' Addnella foreachgenera InvalidOperationException (Collection è stato modificato; operazione di enumerazione non può eseguire), che ritengo logico, dal momento che stiamo tirando il tappeto da sotto i piedi.

Tuttavia, se utilizzo:

var strings = new List<string> { "sample" };
strings.ForEach(s =>
  {
    Console.WriteLine(s);
    strings.Add(s + "!");
  });

prontamente si spara nel piede eseguendo un ciclo finché non genera un'eccezione OutOfMemoryException.

Questa è una sorpresa per me, poiché ho sempre pensato che List.ForEach fosse solo un wrapper per foreacho per for.
Qualcuno ha una spiegazione per il come e il perché di questo comportamento?

(Ispirato dal ciclo ForEach per un elenco generico ripetuto all'infinito )


7
Sono d'accordo. Questo è - dubbio. Desidero che lo pubblichi su microsoft connect e chiedi chiarimenti.
TomTom

4
"Questa è una sorpresa per me, poiché ho sempre pensato che List.ForEach fosse solo un involucro per foreacho per for." Potrebbe ancora usare for. È possibile eseguire la stessa azione in un forciclo e generare di conseguenza la stessa OutOfMemoryException.
Anthony Pegram,

Questo si basa sulla mia domanda: stackoverflow.com/q/9311272/132239 , grazie Sweko per entrare in esso di dettagli
Kasrak

Risposte:


68

È perché il ForEachmetodo non utilizza l'enumeratore, scorre gli elementi con un forciclo:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

(codice ottenuto con JustDecompile)

Poiché l'enumeratore non viene utilizzato, non controlla mai se l'elenco è cambiato e la condizione di fine del forciclo non viene mai raggiunta perché _sizeviene aumentata ad ogni iterazione.


Già, ma come si _sizecalcola? Se è solo pre-calcolato, dovrebbe essere eseguito solo una volta per il mio esempio. È ovviamente aggiornato in qualche modo.
SWeko

7
Viene aggiornato in Add method -> this._items [this._size ++] = item;
Fabio

1
@SWeko, non viene calcolato, viene aggiornato ogni volta che un elemento viene aggiunto o rimosso.
Thomas Levesque,

1
Esiste una _versionvariabile privata in List<T>grado di rilevare questo tipo di scenari, poiché viene aggiornata sulle operazioni che modificano l'elenco stesso.
SWeko

Potresti evitare le eccezioni ottenendo prima la dimensione (int theSize = this._size), quindi usandola all'interno del ciclo for?
Lazlow

14

List<T>.ForEachè implementato tramite forinside, quindi non utilizza enumeratori e permette di modificare la collezione.


6

Perché ForEach collegato alla classe List utilizza internamente un ciclo for che è direttamente collegato ai suoi membri interni, che puoi vedere scaricando il codice sorgente per il framework .NET.

http://referencesource.microsoft.com/netframework.aspx

Dove come un ciclo foreach è prima di tutto un'ottimizzazione del compilatore, ma deve anche operare contro la raccolta come osservatore, quindi se la raccolta viene modificata genera un'eccezione.


E per rispondere al commento sul post di @Thomas su come si aggiorna: i membri interni vengono aggiornati quando viene chiamato add, ecco perché è in grado di stare al passo con le modifiche. Se dovessi eseguire un inserimento, a un indice inferiore a quello attuale, non opereresti mai su quell'elemento perché è già iterato oltre quell'elemento. Ma dal momento che stai aggiungendo alla fine funziona.
Mike Perrenoud,

1
Sì, cambiando la Addriga strings.Insert(0, s + "!")stampando solo "campione". È strano che questo non sia affatto menzionato nella documentazione.
SWeko

Bene, penso che Microsoft abbia capito che è quasi impossibile fornire ogni avvertimento che esiste nella loro documentazione, quindi ora forniscono il loro codice sorgente. Trovo che una soluzione migliore onestamente, ma l'unico problema che ho riscontrato è che prodotti come WF non vengono aggiornati così rapidamente - il codice sorgente 4.x WF non è ancora disponibile.
Mike Perrenoud,

4

Conosciamo questo problema, era una svista quando è stato originariamente scritto. Sfortunatamente, non possiamo cambiarlo perché ora impedirebbe l'esecuzione di questo codice precedentemente funzionante:

        var list = new List<string>();
        list.Add("Foo");
        list.Add("Bar");

        list.ForEach((item) => 
        { 
            if(item=="Foo") 
                list.Remove(item); 
        });

L'utilità di questo metodo stesso è discutibile, come ha sottolineato Eric Lippert , quindi non lo abbiamo incluso per .NET per le app in stile Metro (cioè le app di Windows 8).

David Kean (squadra BCL)


1
Vedo che questo sarebbe un grande cambiamento di rottura, ma ciononostante può fallire in modi non ovvi, e non è mai una buona cosa. Non riesco a vedere uno scenario in cui l'utilizzo del metodo ForEach è superiore a un semplice for (o foreach se non è richiesta la modifica dell'elenco originale)
SWeko
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.