Come si suol dire, il diavolo è nei dettagli ...
La più grande differenza tra i due metodi di enumerazione delle raccolte è che foreach
porta lo stato, mentreForEach(x => { })
non lo è.
Ma cerchiamo di scavare un po 'più a fondo, perché ci sono alcune cose di cui dovresti essere a conoscenza che possono influenzare la tua decisione, e ci sono alcune avvertenze di cui dovresti essere consapevole quando codifichi per entrambi i casi.
Usiamo List<T>
nel nostro piccolo esperimento per osservare il comportamento. Per questo esperimento, sto usando .NET 4.7.2:
var names = new List<string>
{
"Henry",
"Shirley",
"Ann",
"Peter",
"Nancy"
};
Consente di ripetere questo con foreach
prima:
foreach (var name in names)
{
Console.WriteLine(name);
}
Potremmo espandere questo in:
using (var enumerator = names.GetEnumerator())
{
}
Con l'enumeratore in mano, guardando sotto le coperte otteniamo:
public List<T>.Enumerator GetEnumerator()
{
return new List<T>.Enumerator(this);
}
internal Enumerator(List<T> list)
{
this.list = list;
this.index = 0;
this.version = list._version;
this.current = default (T);
}
public bool MoveNext()
{
List<T> list = this.list;
if (this.version != list._version || (uint) this.index >= (uint) list._size)
return this.MoveNextRare();
this.current = list._items[this.index];
++this.index;
return true;
}
object IEnumerator.Current
{
{
if (this.index == 0 || this.index == this.list._size + 1)
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
return (object) this.Current;
}
}
Due cose diventano immediatamente evidenti:
- Ci viene restituito un oggetto con stato con profonda conoscenza della collezione sottostante.
- La copia della collezione è una copia superficiale.
Questo ovviamente non è sicuro per i thread. Come è stato sottolineato sopra, cambiare la raccolta durante l'iterazione è semplicemente un cattivo mojo.
Ma che dire del problema della raccolta che diventa invalido durante l'iterazione per mezzo di noi che confondiamo la raccolta durante l'iterazione? Le migliori pratiche suggeriscono il controllo delle versioni della raccolta durante le operazioni e l'iterazione e il controllo delle versioni per rilevare quando cambia la raccolta sottostante.
Ecco dove le cose diventano davvero oscure. Secondo la documentazione Microsoft:
Se vengono apportate modifiche alla raccolta, ad esempio l'aggiunta, la modifica o l'eliminazione di elementi, il comportamento dell'enumeratore non è definito.
Bene, cosa significa? A titolo di esempio, solo perché List<T>
implementa la gestione delle eccezioni non significa che tutte le raccolte che implementano IList<T>
faranno lo stesso. Questa sembra essere una chiara violazione del principio di sostituzione di Liskov:
Gli oggetti di una superclasse devono essere sostituibili con oggetti delle sue sottoclassi senza interrompere l'applicazione.
Un altro problema è che l'enumeratore deve implementare IDisposable
- ciò significa che un'altra fonte di potenziali perdite di memoria, non solo se il chiamante sbaglia, ma se l'autore non implementa Dispose
correttamente il modello.
Infine, abbiamo un problema a vita ... cosa succede se l'iteratore è valido, ma la raccolta sottostante è sparita? Ora un'istantanea di ciò che era ... quando separi la vita di una collezione e dei suoi iteratori, stai chiedendo problemi.
Ora esaminiamo ForEach(x => { })
:
names.ForEach(name =>
{
});
Questo si espande a:
public void ForEach(Action<T> action)
{
if (action == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
int version = this._version;
for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
action(this._items[index]);
if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
return;
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}
Di nota importante è il seguente:
for (int index = 0; index < this._size && ... ; ++index)
action(this._items[index]);
Questo codice non alloca alcun enumeratore (niente a Dispose
) e non si interrompe durante l'iterazione.
Si noti che ciò esegue anche una copia superficiale della raccolta sottostante, ma la raccolta è ora un'istantanea nel tempo. Se l'autore non implementa correttamente un controllo per la modifica della raccolta o diventa "obsoleto", l'istantanea è ancora valida.
Questo non ti protegge in alcun modo dal problema dei problemi della vita ... se la raccolta sottostante scompare, ora hai una copia superficiale che punta a ciò che era ... ma almeno non hai un Dispose
problemi a trattare con iteratori orfani ...
Sì, ho detto iteratori ... a volte è vantaggioso avere uno stato. Supponiamo di voler mantenere qualcosa di simile a un cursore del database ... forse lo foreach
stile multiplo Iterator<T>
è la strada da percorrere. Personalmente non mi piace questo stile di design in quanto ci sono troppi problemi di vita, e tu fai affidamento sulle buone grazie degli autori delle collezioni su cui fai affidamento (a meno che tu non scriva letteralmente tutto da solo).
C'è sempre una terza opzione ...
for (var i = 0; i < names.Count; i++)
{
Console.WriteLine(names[i]);
}
Non è sexy, ma ha i denti (scuse a Tom Cruise e al film The Firm )
È una tua scelta, ma ora lo sai e può essere informato.