foreach vs someList.ForEach () {}


167

Apparentemente ci sono molti modi per scorrere su una raccolta. Curioso se ci sono differenze o perché dovresti usare un modo rispetto all'altro.

Primo tipo:

List<string> someList = <some way to init>
foreach(string s in someList) {
   <process the string>
}

Altro modo:

List<string> someList = <some way to init>
someList.ForEach(delegate(string s) {
    <process the string>
});

Suppongo che, al di fuori del delegato anonimo che uso sopra, avresti un delegato riutilizzabile che potresti specificare ...


Risposte:


134

C'è una distinzione importante e utile tra i due.

Poiché .ForEach utilizza un forciclo per iterare la raccolta, questo è valido (modifica: prima di .net 4.5 - l'implementazione è cambiata ed entrambi lanciano):

someList.ForEach(x => { if(x.RemoveMe) someList.Remove(x); }); 

considerando che foreachutilizza un enumeratore, quindi questo non è valido:

foreach(var item in someList)
  if(item.RemoveMe) someList.Remove(item);

tl; dr: NON copiare questo codice nella tua applicazione!

Questi esempi non sono le migliori pratiche, sono solo per dimostrare le differenze tra ForEach()e foreach.

La rimozione di elementi da un elenco all'interno di un forciclo può avere effetti collaterali. Il più comune è descritto nei commenti a questa domanda.

In genere, se si desidera rimuovere più elementi da un elenco, si desidera separare la determinazione di quali elementi rimuovere dalla rimozione effettiva. Non mantiene il codice compatto, ma garantisce di non perdere alcun elemento.


35
anche allora, dovresti usare someList.RemoveAll (x => x.RemoveMe) invece
Mark Cidade,

2
Con Linq, tutto può essere fatto meglio. Stavo solo mostrando un esempio di modifica della raccolta all'interno di foreach ...

2
RemoveAll () è un metodo in Elenco <T>.
Mark Cidade,

3
Probabilmente ne sei consapevole, ma le persone dovrebbero fare attenzione a rimuovere gli oggetti in questo modo; se rimuovi l'elemento N, l'iterazione salterà sull'elemento (N + 1) e non lo vedrai nel tuo delegato o non avrai la possibilità di rimuoverlo, proprio come se lo avessi fatto nel tuo ciclo.
El Zorko,

9
Se esegui l'iterazione all'indietro dell'elenco con un ciclo for, puoi rimuovere gli elementi senza problemi di spostamento dell'indice.
S. Tarık Çetin,

78

Avevamo un po 'di codice qui (in VS2005 e C # 2.0) in cui i precedenti ingegneri si facevano in quattro per usarlo list.ForEach( delegate(item) { foo;});invece che foreach(item in list) {foo; };per tutto il codice che avevano scritto. ad esempio un blocco di codice per la lettura di righe da un dataReader.

Non so ancora esattamente perché l'hanno fatto.

Gli svantaggi di list.ForEach()sono:

  • È più dettagliato in C # 2.0. Tuttavia, in C # 3 in poi, è possibile utilizzare la =>sintassi " " per rendere alcune espressioni ben concise.

  • È meno familiare. Le persone che devono mantenere questo codice si chiederanno perché l'hai fatto in quel modo. Mi ci è voluto un po 'per decidere che non c'era alcun motivo, tranne forse per far sembrare intelligente lo scrittore (la qualità del resto del codice lo ha minato). Era anche meno leggibile, con " })" alla fine del blocco di codice delegato.

  • Vedi anche il libro di Bill Wagner "Efficace C #: 50 modi specifici per migliorare il tuo C #" in cui parla del perché foreach è preferito ad altri loop come per o while loop - il punto principale è che stai lasciando che il compilatore decida il modo migliore per costruire il cappio. Se una versione futura del compilatore riesce a utilizzare una tecnica più veloce, la otterrai gratuitamente utilizzando foreach e ricostruzione, anziché modificare il codice.

  • un foreach(item in list)costrutto ti consente di usare breako continuese devi uscire dall'iterazione o dal ciclo. Ma non è possibile modificare l'elenco all'interno di un ciclo foreach.

Sono sorpreso di vedere che list.ForEachè leggermente più veloce. Ma questo probabilmente non è un motivo valido per usarlo in tutto, sarebbe l'ottimizzazione prematura. Se la tua applicazione utilizza un database o un servizio web che, non il controllo di loop, sarà quasi sempre dove il tempo passa. E lo hai confrontato anche con un forloop? Il list.ForEachpotrebbe essere più veloce a causa di uso che internamente ed unfor ciclo senza l'involucro sarebbe ancora più veloce.

Non sono d'accordo sul fatto che la list.ForEach(delegate)versione sia "più funzionale" in alcun modo significativo. Passa una funzione a una funzione, ma non c'è grande differenza nel risultato o nell'organizzazione del programma.

Non penso che foreach(item in list)"dica esattamente come lo vuoi fare" - un for(int 1 = 0; i < count; i++)loop lo fa, un foreachloop lascia la scelta del controllo al compilatore.

La mia sensazione è, su un nuovo progetto, di utilizzare foreach(item in list)per la maggior parte dei loop per aderire all'uso comune e per la leggibilità e utilizzare list.Foreach()solo per blocchi brevi, quando è possibile fare qualcosa di più elegante o compatto con l' =>operatore C # 3 " . In casi del genere, potrebbe esserci già un metodo di estensione LINQ più specifico di ForEach(). Vedere se Where(), Select(), Any(), All(), Max()o uno dei molti altri metodi di LINQ non già fare quello che vuoi dal loop.


Solo per curiosità ... guarda l'implementazione di Microsoft ... riferimentiource.microsoft.com/#mscorlib/system/collections/…
Alexandre

17

Per divertimento, ho inserito List in reflector e questo è il C # risultante:

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]);
    }
}

Allo stesso modo, MoveNext in Enumerator che è ciò che viene utilizzato da foreach è questo:

public bool MoveNext()
{
    if (this.version != this.list._version)
    {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }
    if (this.index < this.list._size)
    {
        this.current = this.list._items[this.index];
        this.index++;
        return true;
    }
    this.index = this.list._size + 1;
    this.current = default(T);
    return false;
}

List.ForEach è molto più ridotto rispetto a MoveNext, molto meno elaborato, molto probabilmente JIT in qualcosa di efficiente.

Inoltre, foreach () assegnerà un nuovo enumeratore, non importa quale. Il GC è tuo amico, ma se stai facendo lo stesso foreach ripetutamente, questo renderà più oggetti usa e getta, invece di riutilizzare lo stesso delegato - MA - questo è davvero un caso marginale. Nell'uso tipico vedrai poca o nessuna differenza.


1
Non hai alcuna garanzia che il codice generato da foreach sarà lo stesso tra le versioni del compilatore. Il codice generato potrebbe essere migliorato da una versione futura.
Anthony,

1
A partire da .NET Core 3.1 ForEach è ancora più veloce.
Jackmott

13

Conosco due cose oscure che le rendono diverse. Andatemi!

In primo luogo, c'è il classico bug di creare un delegato per ciascun elemento nell'elenco. Se usi la parola chiave foreach, tutti i tuoi delegati possono finire facendo riferimento all'ultimo elemento dell'elenco:

    // A list of actions to execute later
    List<Action> actions = new List<Action>();

    // Numbers 0 to 9
    List<int> numbers = Enumerable.Range(0, 10).ToList();

    // Store an action that prints each number (WRONG!)
    foreach (int number in numbers)
        actions.Add(() => Console.WriteLine(number));

    // Run the actions, we actually print 10 copies of "9"
    foreach (Action action in actions)
        action();

    // So try again
    actions.Clear();

    // Store an action that prints each number (RIGHT!)
    numbers.ForEach(number =>
        actions.Add(() => Console.WriteLine(number)));

    // Run the actions
    foreach (Action action in actions)
        action();

Il metodo List.ForEach non presenta questo problema. L'elemento corrente dell'iterazione viene passato per valore come argomento alla lambda esterna, quindi la lambda interna acquisisce correttamente tale argomento nella propria chiusura. Problema risolto.

(Purtroppo credo che ForEach sia un membro di List, piuttosto che un metodo di estensione, anche se è facile definirlo da soli in modo da avere questa funzione su qualsiasi tipo enumerabile.)

In secondo luogo, l'approccio del metodo ForEach ha una limitazione. Se si sta implementando IEnumerable utilizzando il rendimento, non è possibile eseguire un rendimento nel lambda. Pertanto, con questo metodo non è possibile eseguire il ciclo tra gli articoli in una raccolta al fine di restituire le cose di ritorno. Dovrai utilizzare la parola chiave foreach e aggirare il problema di chiusura creando manualmente una copia del valore del ciclo corrente all'interno del ciclo.

Più qui


5
Il problema con cui accenni foreachè "risolto" in C # 5. stackoverflow.com/questions/8898925/…
StriplingWarrior il

13

Immagino che la someList.ForEach()chiamata possa essere facilmente parallelizzata mentre la normale foreachnon è così facile da eseguire in parallelo. Si potrebbe facilmente eseguire diversi delegati diversi su core diversi, il che non è così facile da fare con un normale foreach.
Solo i miei 2 centesimi


2
Penso che intendesse dire che il motore di runtime potrebbe parallelizzarlo automaticamente. Altrimenti, sia foreach che .ForEach possono essere parallelizzati manualmente usando un thread dal pool in ciascun delegato dell'azione
Isak Savo

@Isak ma sarebbe un'ipotesi errata. Se il metodo anonimo solleva un locale o un membro il tempo di esecuzione non sarà 8easily) in grado di paralizzare
Rune FS

6

Come si suol dire, il diavolo è nei dettagli ...

La più grande differenza tra i due metodi di enumerazione delle raccolte è che foreachporta 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 foreachprima:

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:

  1. Ci viene restituito un oggetto con stato con profonda conoscenza della collezione sottostante.
  2. 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 Disposecorrettamente 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 foreachstile 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.


5

Potresti nominare il delegato anonimo :-)

E puoi scrivere il secondo come:

someList.ForEach(s => s.ToUpper())

Quale preferisco e risparmia molto la digitazione.

Come dice Joachim, il parallelismo è più facile da applicare alla seconda forma.


2

Dietro le quinte, il delegato anonimo viene trasformato in un metodo reale in modo da poter avere un po 'di sovraccarico con la seconda scelta se il compilatore non scegliesse di incorporare la funzione. Inoltre, qualsiasi variabile locale a cui fa riferimento il corpo dell'esempio del delegato anonimo cambierebbe in natura a causa dei trucchi del compilatore per nascondere il fatto che viene compilato in un nuovo metodo. Maggiori informazioni qui su come C # fa questa magia:

http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx


2

La funzione ForEach è membro dell'elenco di classi generico.

Ho creato la seguente estensione per riprodurre il codice interno:

public static class MyExtension<T>
    {
        public static void MyForEach(this IEnumerable<T> collection, Action<T> action)
        {
            foreach (T item in collection)
                action.Invoke(item);
        }
    }

Quindi alla fine stiamo usando un normale foreach (o un loop per se lo desideri).

D'altra parte, l'utilizzo di una funzione delegata è solo un altro modo per definire una funzione, questo codice:

delegate(string s) {
    <process the string>
}

è equivalente a:

private static void myFunction(string s, <other variables...>)
{
   <process the string>
}

o usando le espressioni labda:

(s) => <process the string>

2

L'intero ambito ForEach (funzione delegata) viene trattato come una singola riga di codice (chiamata alla funzione) e non è possibile impostare punti di interruzione o entrare nel codice. Se si verifica un'eccezione non gestita, l'intero blocco viene contrassegnato.


2

List.ForEach () è considerato più funzionale.

List.ForEach()dice quello che vuoi fare. foreach(item in list)dice anche esattamente come lo vuoi fare. Questo lascia List.ForEachliberi di cambiare l'attuazione del modo in parte in futuro. Ad esempio, un'ipotetica versione futura di .Net potrebbe sempre essere eseguitaList.ForEach in parallelo, supponendo che a questo punto tutti abbiano un numero di core CPU che sono generalmente inattivi.

D'altra parte, foreach (item in list)ti dà un po 'più di controllo sul loop. Ad esempio, sai che gli articoli verranno ripetuti in una sorta di ordine sequenziale e potresti facilmente rompere nel mezzo se un articolo soddisfa una condizione.


Alcune osservazioni più recenti su questo problema sono disponibili qui:

https://stackoverflow.com/a/529197/3043


1

Il secondo modo che hai mostrato utilizza un metodo di estensione per eseguire il metodo delegato per ciascuno degli elementi nell'elenco.

In questo modo, hai un'altra chiamata delegata (= metodo).

Inoltre, esiste la possibilità di iterare l'elenco con un ciclo for .


1

Una cosa di cui diffidare è come uscire dal metodo Generic .ForEach - vedi questa discussione . Anche se il link sembra dire che in questo modo è il più veloce. Non so perché - penseresti che sarebbero equivalenti una volta compilati ...


0

C'è un modo, l'ho fatto nella mia app:

List<string> someList = <some way to init>
someList.ForEach(s => <your actions>);

Puoi usare il tuo articolo come foreach

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.