foreach vs someList.ForEach () {}

167

Es gibt anscheinend viele Möglichkeiten, eine Sammlung zu durchlaufen. Neugierig, ob es Unterschiede gibt oder warum Sie einen Weg über den anderen verwenden würden.

Erster Typ:

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

Andere Weise:

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

Ich nehme an, dass Sie anstelle des oben verwendeten anonymen Delegaten einen wiederverwendbaren Delegierten haben würden, den Sie angeben könnten ...

Bryce Fischer
quelle
1
Ich schlage vor, Eric Lipperts Blog "foreach" vs "ForEach"
Erik Philips

Antworten:

134

Es gibt eine wichtige und nützliche Unterscheidung zwischen den beiden.

Da .ForEach eine forSchleife verwendet, um die Sammlung zu iterieren, ist dies gültig (edit: vor .net 4.5 - die Implementierung wurde geändert und beide werfen):

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

Während foreachein Enumerator verwendet wird, ist dies nicht gültig:

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

tl; dr: Kopieren Sie diesen Code NICHT in Ihre Anwendung!

Diese Beispiele sind keine bewährten Methoden, sondern dienen lediglich der Veranschaulichung der Unterschiede zwischen ForEach()und foreach.

Das Entfernen von Elementen aus einer Liste innerhalb einer forSchleife kann Nebenwirkungen haben. Die häufigste ist in den Kommentaren zu dieser Frage beschrieben.

Wenn Sie mehrere Elemente aus einer Liste entfernen möchten, möchten Sie im Allgemeinen die Bestimmung der zu entfernenden Elemente von der tatsächlichen Entfernung trennen. Es hält Ihren Code nicht kompakt, garantiert aber, dass Sie keine Elemente verpassen.


quelle
35
Selbst dann sollten Sie stattdessen someList.RemoveAll (x => x.RemoveMe) verwenden
Mark Cidade
2
Mit Linq können alle Dinge besser gemacht werden. Ich habe nur ein Beispiel für das Ändern der Sammlung in foreach gezeigt ...
2
RemoveAll () ist eine Methode in List <T>.
Mark Cidade
3
Sie sind sich dessen wahrscheinlich bewusst, aber die Leute sollten aufpassen, dass sie Gegenstände auf diese Weise entfernen. Wenn Sie Element N entfernen, überspringt die Iteration das Element (N + 1) und Sie sehen es nicht in Ihrem Delegaten oder erhalten die Möglichkeit, es zu entfernen, so als ob Sie dies in Ihrer eigenen for-Schleife getan hätten.
El Zorko
9
Wenn Sie die Liste mit einer for-Schleife rückwärts durchlaufen , können Sie Elemente ohne Probleme mit der Indexverschiebung entfernen.
S. Tarık Çetin
78

Wir hatten hier Code (in VS2005 und C # 2.0), bei dem die vorherigen Ingenieure alles daran gesetzt haben, list.ForEach( delegate(item) { foo;});anstelle des foreach(item in list) {foo; };gesamten von ihnen geschriebenen Codes zu verwenden. zB ein Codeblock zum Lesen von Zeilen aus einem dataReader.

Ich weiß immer noch nicht genau, warum sie das getan haben.

Die Nachteile von list.ForEach()sind:

  • In C # 2.0 ist es ausführlicher. Ab C # 3 können Sie jedoch die =>Syntax " " verwenden, um einige schöne, knappe Ausdrücke zu erstellen.

  • Es ist weniger bekannt. Leute, die diesen Code pflegen müssen, werden sich fragen, warum Sie das so gemacht haben. Ich brauchte eine Weile, um zu entscheiden, dass es keinen Grund gab, außer vielleicht, um den Autor klug erscheinen zu lassen (die Qualität des restlichen Codes untergrub dies). Es war auch weniger lesbar, mit dem " })" am Ende des Delegatencodeblocks.

  • Siehe auch Bill Wagners Buch "Effektives C #: 50 spezifische Möglichkeiten zur Verbesserung Ihres C #", in dem er darüber spricht, warum foreach anderen Schleifen wie for- oder while-Schleifen vorgezogen wird - der Hauptpunkt ist, dass Sie den Compiler entscheiden lassen, wie am besten konstruiert werden soll die Schleife. Wenn es einer zukünftigen Version des Compilers gelingt, eine schnellere Technik zu verwenden, erhalten Sie diese kostenlos, indem Sie foreach verwenden und neu erstellen, anstatt Ihren Code zu ändern.

  • ein foreach(item in list)Konstrukt , können Sie verwenden breakoder continuewenn Sie die Iteration oder die Schleife beenden müssen. Sie können die Liste jedoch nicht in einer foreach-Schleife ändern.

Ich bin überrascht zu sehen, dass list.ForEachdas etwas schneller ist. Aber das ist wahrscheinlich kein triftiger Grund, es durchgehend zu verwenden, das wäre eine vorzeitige Optimierung. Wenn Ihre Anwendung eine Datenbank oder einen Webdienst verwendet, bei dem es sich nicht um eine Schleifensteuerung handelt, ist dies fast immer der Ort, an dem die Zeit vergeht. Und haben Sie es auch mit einer forSchleife verglichen? Das list.ForEachkönnte schneller sein, weil es intern verwendet wird, und eine forSchleife ohne den Wrapper wäre noch schneller.

Ich bin nicht der Meinung, dass die list.ForEach(delegate)Version in irgendeiner Weise "funktionaler" ist. Es wird zwar eine Funktion an eine Funktion übergeben, es gibt jedoch keinen großen Unterschied im Ergebnis oder in der Programmorganisation.

Ich glaube nicht, dass foreach(item in list)"genau sagt, wie Sie es wollen" - eine for(int 1 = 0; i < count; i++)Schleife macht das, eine foreachSchleife überlässt die Wahl der Steuerung dem Compiler.

Ich habe das Gefühl, bei einem neuen Projekt foreach(item in list)für die meisten Schleifen zu verwenden, um die allgemeine Verwendung und Lesbarkeit einzuhalten, und list.Foreach()nur für kurze Blöcke zu verwenden, wenn Sie mit dem C # 3 " =>" -Operator etwas eleganteres oder kompakteres tun können . In solchen Fällen gibt es möglicherweise bereits eine LINQ-Erweiterungsmethode, die spezifischer ist als ForEach(). Sehen Sie, wenn Where(), Select(), Any(), All(), Max()oder eine der vielen anderen LINQ Methoden nicht bereits tun , was Sie aus der Schleife möchten.

Anthony
quelle
Nur aus Neugier ... schauen Sie sich die Microsoft-Implementierung an ... referencesource.microsoft.com/#mscorlib/system/collections/…
Alexandre
17

Zum Spaß habe ich List in den Reflektor gesteckt und dies ist das resultierende C #:

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

In ähnlicher Weise ist der MoveNext im Enumerator, der von foreach verwendet wird, folgender:

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

Die List.ForEach ist viel kürzer als MoveNext - weit weniger Verarbeitung - wird JIT eher zu etwas Effizientem machen.

Außerdem weist foreach () unabhängig davon einen neuen Enumerator zu. Der GC ist Ihr Freund, aber wenn Sie wiederholt dasselbe tun, werden mehr wegwerfbare Objekte erzeugt, anstatt denselben Delegierten wiederzuverwenden - ABER - dies ist wirklich ein Randfall. Im typischen Gebrauch werden Sie wenig oder keinen Unterschied sehen.

Sockel
quelle
1
Sie können nicht garantieren, dass der von foreach generierte Code zwischen den Compilerversionen identisch ist. Der generierte Code kann durch eine zukünftige Version verbessert werden.
Anthony
1
Ab .NET Core 3.1 ist ForEach noch schneller.
Jackmott
13

Ich kenne zwei obskure Dinge, die sie anders machen. Geh mich!

Erstens gibt es den klassischen Fehler, für jedes Element in der Liste einen Delegaten zu erstellen. Wenn Sie das Schlüsselwort foreach verwenden, verweisen alle Ihre Delegierten möglicherweise auf das letzte Element der Liste:

    // 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();

Die List.ForEach-Methode hat dieses Problem nicht. Das aktuelle Element der Iteration wird als Argument an das äußere Lambda übergeben, und das innere Lambda erfasst dieses Argument dann korrekt in seinem eigenen Abschluss. Problem gelöst.

(Leider glaube ich, dass ForEach eher ein Mitglied von List als eine Erweiterungsmethode ist, obwohl es einfach ist, es selbst zu definieren, sodass Sie diese Funktion für jeden aufzählbaren Typ haben.)

Zweitens weist der ForEach-Methodenansatz eine Einschränkung auf. Wenn Sie IEnumerable mithilfe von Yield Return implementieren, können Sie innerhalb des Lambda keine Yield Return durchführen. Ein Durchlaufen der Elemente in einer Sammlung, um Rückgaben zu erzielen, ist mit dieser Methode nicht möglich. Sie müssen das Schlüsselwort foreach verwenden und das Schließproblem umgehen, indem Sie manuell eine Kopie des aktuellen Schleifenwerts innerhalb der Schleife erstellen.

Mehr hier

Daniel Earwicker
quelle
5
Das Problem, mit dem Sie erwähnen, foreachist in C # 5 "behoben". Stackoverflow.com/questions/8898925/…
StriplingWarrior
13

Ich denke, der someList.ForEach()Anruf könnte leicht parallelisiert werden, während das normale foreachnicht so einfach parallel zu laufen ist. Sie können problemlos mehrere verschiedene Delegaten auf verschiedenen Kernen ausführen, was mit einem normalen System nicht so einfach ist foreach.
Nur meine 2 Cent

Joachim Kerschbaumer
quelle
2
Ich denke, er meinte, dass die Laufzeit-Engine sie automatisch parallelisieren könnte. Andernfalls können sowohl foreach als auch .ForEach von Hand mithilfe eines Threads aus dem Pool in jedem Aktionsdelegierten parallelisiert werden
Isak Savo,
@Isak aber das wäre eine falsche Annahme. Wenn die anonyme Methode einen Einheimischen oder ein Mitglied hochzieht, kann die Laufzeit nicht einfach paralellisieren
Rune FS
6

Wie sie sagen, der Teufel steckt im Detail ...

Der größte Unterschied zwischen den beiden Methoden der Aufzählung von Sammlungen besteht darin, dass foreachder Status übertragen wird, während ForEach(x => { })dies nicht der Fall ist.

Aber lassen Sie uns etwas tiefer gehen, denn es gibt einige Dinge, die Sie beachten sollten, die Ihre Entscheidung beeinflussen können, und es gibt einige Einschränkungen, die Sie beim Codieren für beide Fälle beachten sollten.

Verwenden List<T>wir in unserem kleinen Experiment das Verhalten. Für dieses Experiment verwende ich .NET 4.7.2:

var names = new List<string>
{
    "Henry",
    "Shirley",
    "Ann",
    "Peter",
    "Nancy"
};

Lassen Sie uns dies foreachzuerst wiederholen :

foreach (var name in names)
{
    Console.WriteLine(name);
}

Wir könnten dies erweitern in:

using (var enumerator = names.GetEnumerator())
{

}

Mit dem Enumerator in der Hand sehen wir unter die Decke:

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

Zwei Dinge werden sofort offensichtlich:

  1. Wir erhalten ein zustandsbehaftetes Objekt mit vertrauter Kenntnis der zugrunde liegenden Sammlung zurück.
  2. Die Kopie der Sammlung ist eine flache Kopie.

Dies ist natürlich in keiner Weise threadsicher. Wie oben erwähnt, ist das Ändern der Sammlung während des Iterierens nur ein schlechtes Mojo.

Aber was ist mit dem Problem, dass die Sammlung während der Iteration ungültig wird, wenn wir nicht mit der Sammlung während der Iteration herumspielen? Best Practices empfehlen, die Sammlung während des Betriebs und der Iteration zu versionieren und Versionen zu überprüfen, um festzustellen, wann sich die zugrunde liegende Sammlung ändert.

Hier wird es richtig trübe. Laut Microsoft-Dokumentation:

Wenn Änderungen an der Auflistung vorgenommen werden, z. B. das Hinzufügen, Ändern oder Löschen von Elementen, ist das Verhalten des Enumerators nicht definiert.

Was bedeutet das? Nur weil List<T>die Ausnahmebehandlung implementiert IList<T>wird, bedeutet dies nicht, dass alle Sammlungen, die implementiert werden, dasselbe tun. Dies scheint ein klarer Verstoß gegen das Liskov-Substitutionsprinzip zu sein:

Gegenstände einer Oberklasse müssen durch Gegenstände ihrer Unterklassen ersetzt werden können, ohne die Anwendung zu verletzen.

Ein weiteres Problem besteht darin, dass der Enumerator implementiert werden muss. Dies IDisposablebedeutet, dass eine andere Quelle potenzieller Speicherlecks auftritt, nicht nur, wenn der Aufrufer etwas falsch macht, sondern wenn der Autor das DisposeMuster nicht korrekt implementiert .

Schließlich haben wir ein lebenslanges Problem ... Was passiert, wenn der Iterator gültig ist, die zugrunde liegende Sammlung jedoch nicht mehr vorhanden ist? Wir haben jetzt eine Momentaufnahme dessen, was war ... Wenn Sie die Lebensdauer einer Sammlung von ihren Iteratoren trennen, fragen Sie nach Problemen.

Untersuchen wir nun ForEach(x => { }):

names.ForEach(name =>
{

});

Dies erweitert sich auf:

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

Von wichtiger Bedeutung ist Folgendes:

for (int index = 0; index < this._size && ... ; ++index) action(this._items[index]);

Dieser Code weist keine Enumeratoren zu (nichts zu Dispose) und pausiert nicht während der Iteration.

Beachten Sie, dass hierdurch auch eine flache Kopie der zugrunde liegenden Sammlung erstellt wird, die Sammlung jedoch jetzt eine Momentaufnahme ist. Wenn der Autor eine Überprüfung, ob sich die Sammlung ändert oder veraltet ist, nicht korrekt implementiert, ist der Schnappschuss weiterhin gültig.

Dies schützt Sie in keiner Weise vor dem Problem der lebenslangen Probleme. Wenn die zugrunde liegende Sammlung verschwindet, haben Sie jetzt eine flache Kopie, die auf das verweist, was war. Aber zumindest haben Sie kein DisposeProblem damit beschäftigen sich mit verwaisten Iteratoren ...

Ja, ich sagte Iteratoren ... manchmal ist es vorteilhaft, einen Zustand zu haben. Angenommen, Sie möchten etwas beibehalten, das einem Datenbankcursor ähnelt. Vielleicht sind mehrere foreachStile Iterator<T>der richtige Weg. Ich persönlich mag diesen Designstil nicht, da es zu viele Probleme auf Lebenszeit gibt und Sie sich auf die guten Seiten der Autoren der Sammlungen verlassen, auf die Sie sich verlassen (es sei denn, Sie schreiben buchstäblich alles selbst von Grund auf neu).

Es gibt immer eine dritte Option ...

for (var i = 0; i < names.Count; i++)
{
    Console.WriteLine(names[i]);
}

Es ist nicht sexy, aber es hat Zähne (Entschuldigung an Tom Cruise und den Film The Firm )

Es ist Ihre Wahl, aber jetzt wissen Sie es und es kann eine informierte sein.

Stacy Dudovitz
quelle
5

Sie könnten den anonymen Delegierten benennen :-)

Und Sie können die zweite schreiben als:

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

Was ich bevorzuge und viel Tipparbeit erspare.

Wie Joachim sagt, lässt sich Parallelität leichter auf die zweite Form anwenden.

Craig.Nicol
quelle
2

Hinter den Kulissen wird der anonyme Delegat zu einer tatsächlichen Methode, sodass Sie bei der zweiten Auswahl einen gewissen Aufwand haben können, wenn der Compiler die Funktion nicht inline verwendet. Darüber hinaus würden sich alle lokalen Variablen, auf die der Hauptteil des anonymen Delegatenbeispiels verweist, aufgrund von Compilertricks ändern, um die Tatsache zu verbergen, dass sie zu einer neuen Methode kompiliert werden. Weitere Informationen dazu, wie C # diese Magie ausübt:

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

jezell
quelle
2

Die ForEach-Funktion ist Mitglied der generischen Klassenliste.

Ich habe die folgende Erweiterung erstellt, um den internen Code zu reproduzieren:

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

Am Ende verwenden wir also ein normales foreach (oder eine Schleife, wenn Sie möchten).

Auf der anderen Seite ist die Verwendung einer Delegatenfunktion nur eine andere Möglichkeit, eine Funktion zu definieren. Dieser Code:

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

ist äquivalent zu:

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

oder mit Labda-Ausdrücken:

(s) => <process the string>
Pablo Caballero
quelle
2

Der gesamte ForEach-Bereich (Delegate-Funktion) wird als einzelne Codezeile behandelt (Aufruf der Funktion), und Sie können keine Haltepunkte festlegen oder in den Code eintreten. Wenn eine nicht behandelte Ausnahme auftritt, wird der gesamte Block markiert.

Peter Shen
quelle
2

List.ForEach () wird als funktionaler angesehen.

List.ForEach()sagt, was du tun willst. foreach(item in list)sagt auch genau, wie du es machen willst. Dies lässt die List.ForEachMöglichkeit, die Implementierung des Wie- Teils in der Zukunft zu ändern . Beispielsweise kann eine hypothetische zukünftige Version von .Net immer List.ForEachparallel ausgeführt werden, unter der Annahme, dass zu diesem Zeitpunkt jeder über eine Reihe von CPU-Kernen verfügt, die im Allgemeinen im Leerlauf sitzen.

Auf der anderen Seite haben foreach (item in list)Sie etwas mehr Kontrolle über die Schleife. Sie wissen beispielsweise, dass die Elemente in einer sequentiellen Reihenfolge iteriert werden, und Sie können leicht in der Mitte brechen, wenn ein Element eine bestimmte Bedingung erfüllt.


Einige neuere Anmerkungen zu diesem Thema finden Sie hier:

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

Joel Coehoorn
quelle
1

Die zweite Möglichkeit, die Sie gezeigt haben, verwendet eine Erweiterungsmethode, um die Delegatmethode für jedes der Elemente in der Liste auszuführen.

Auf diese Weise haben Sie einen weiteren Delegatenaufruf (= Methode).

Zusätzlich besteht die Möglichkeit, die Liste mit einer for- Schleife zu iterieren .

EFrank
quelle
1

Eine Sache, vor der Sie vorsichtig sein sollten, ist das Beenden der generischen .ForEach-Methode - siehe diese Diskussion . Obwohl der Link zu sagen scheint, dass dieser Weg der schnellste ist. Ich weiß nicht warum - Sie würden denken, dass sie nach der Kompilierung gleichwertig wären ...

Chris Kimpton
quelle
0

Es gibt einen Weg, den ich in meiner App gemacht habe:

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

Sie können das als Ihren Artikel aus dem Foreach verwenden

Dannick Bédard
quelle