196 Stimmen

Foreach vs someList.ForEach(){}

Es gibt anscheinend viele Möglichkeiten, über eine Sammlung zu iterieren. Ich frage mich, ob es Unterschiede gibt oder warum man eine Methode der anderen vorziehen würde.

Erster Typ:

List someList = 
foreach(string s in someList) {

}

Andere Möglichkeit:

List someList = 
someList.ForEach(delegate(string s) {

});

Ich nehme an, dass anstatt des anonymen Delegaten, den ich oben benutze, einen wieder verwendbaren Delegaten verwenden könnten...

150voto

Es gibt einen wichtigen und nützlichen Unterschied zwischen den beiden.

Weil .ForEach eine for-Schleife verwendet, um die Sammlung zu durchlaufen, ist dies gültig (Bearbeitung: vor .NET 4.5 - die Implementierung hat sich geändert und sie werfen beide):

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

während foreach einen Enumerator verwendet, daher 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ährte Praxis, sondern dienen nur dazu, die Unterschiede zwischen ForEach() und foreach zu demonstrieren.

Das Entfernen von Elementen aus einer Liste innerhalb einer for-Schleife kann Seiteneffekte haben. Der häufigste wird in den Kommentaren zu dieser Frage beschrieben.

Im Allgemeinen, wenn Sie mehrere Elemente aus einer Liste entfernen möchten, sollten Sie die Bestimmung der zu entfernenden Elemente von der tatsächlichen Entfernung trennen. Es macht Ihren Code nicht kompakt, garantiert jedoch, dass Sie keine Elemente verpassen.

85voto

Anthony Punkte 5038

Wir hatten hier etwas Code (in VS2005 und C#2.0), bei dem die vorherigen Ingenieure sich die Mühe gemacht haben, list.ForEach( delegate(item) { foo;}); anstelle von foreach(item in list) {foo;}; für den gesamten Code, den sie geschrieben haben, zu verwenden. z.B. ein Block von Code zum Lesen von Zeilen aus einem dataReader.

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

Die Nachteile von list.ForEach() sind:

  • Es ist umständlicher in C# 2.0. In C# 3 und höher können Sie jedoch die "=>"-Syntax verwenden, um einige schön knappe Ausdrücke zu erstellen.

  • Es ist weniger vertraut. Personen, die diesen Code warten müssen, werden sich fragen, warum Sie es auf diese Weise gemacht haben. Es hat mich eine Weile gekostet zu entscheiden, dass es keinen Grund dafür gab, außer vielleicht um den Verfasser schlau erscheinen zu lassen (die Qualität des restlichen Codes hat das untergraben). Es war auch weniger lesbar, mit dem "})" am Ende des Delegaten-Codeblocks.

  • Siehe auch Bill Wagners Buch "Effektives C#: 50 spezifische Möglichkeiten, Ihr C# zu verbessern", wo er erklärt, warum foreach gegenüber anderen Schleifen wie for oder while bevorzugt wird - der Hauptpunkt ist, dass Sie den Compiler entscheiden lassen, wie die Schleife am besten aufgebaut wird. Wenn eine zukünftige Version des Compilers eine schnellere Technik verwenden kann, erhalten Sie dies kostenlos, wenn Sie foreach verwenden und neu erstellen, anstatt Ihren Code zu ändern.

  • Ein foreach(item in list)-Konstrukt ermöglicht es Ihnen, break oder continue zu verwenden, wenn Sie die Iteration oder die Schleife beenden müssen. Aber Sie können die Liste nicht innerhalb einer foreach-Schleife ändern.

Ich bin überrascht zu sehen, dass list.ForEach etwas schneller ist. Aber das ist wahrscheinlich kein gültiger Grund, es überall zu verwenden, das wäre vorzeitige Optimierung. Wenn Ihre Anwendung eine Datenbank oder einen Webdienst verwendet, wird nicht die Schleifensteuerung fast immer der Zeitpunkt sein, an dem die Zeit verstreicht. Haben Sie es auch mit einer for-Schleife verglichen? Der list.ForEach könnte schneller sein, weil er intern das verwendet und eine for-Schleife ohne Wrapper wäre noch schneller.

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

Ich glaube nicht, dass foreach(item in list) "genau sagt, wie Sie es möchten" - eine for(int i = 0; i < count; i++)-Schleife tut dies, eine foreach-Schleife überlässt die Kontrolle dem Compiler.

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

18voto

plinth Punkte 46829

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

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

Ähnlich wie das MoveNext im Enumerator, das von foreach verwendet wird, sieht so aus:

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

Das List.ForEach ist viel kürzer als MoveNext - viel weniger Verarbeitung - wird wahrscheinlich eher in etwas Effizientes kompiliert.

Zusätzlich wird bei foreach() immer ein neuer Enumerator allokiert. Der Garbage Collector ist dein Freund, aber wenn du dasselbe foreach wiederholt machst, werden mehr Objekte weggeworfen, anstatt den gleichen Delegaten wiederzuverwenden - ABER - das ist wirklich ein Randfall. Bei typischer Verwendung wirst du keine oder nur geringfügige Unterschiede feststellen.

15voto

Stacy Dudovitz Punkte 764

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

Der größte Unterschied zwischen den beiden Methoden der Aufzählung von Sammlungen ist, dass foreach Zustand trägt, wohingegen ForEach(x => { }) das nicht tut.

Aber lassen Sie uns etwas genauer hinschauen, denn es gibt einige Dinge, die Sie beachten sollten, die Ihre Entscheidung beeinflussen können, und es gibt einige Vorbehalte, die Sie beachten sollten, wenn Sie für einen der Fälle programmieren.

Lassen Sie uns List in unserem kleinen Experiment verwenden, um das Verhalten zu beobachten. Für dieses Experiment verwende ich .NET 4.7.2:

var namen = new List
{
    "Henry",
    "Shirley",
    "Ann",
    "Peter",
    "Nancy"
};

Lassen Sie uns zuerst mit foreach darüber iterieren:

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

Wir könnten dies erweitern zu:

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

}

Mit dem Enumerator in der Hand, wenn wir unter die Oberfläche schauen, erhalten wir:

public List.Enumerator GetEnumerator()
{
  return new List.Enumerator(this);
}
    internal Enumerator(List liste)
{
  this.list = liste;
  this.index = 0;
  this.version = liste._version;
  this.current = default (T);
}

public bool MoveNext()
{
  List liste = this.list;
  if (this.version != liste._version || (uint) this.index >= (uint) liste._size)
    return this.MoveNextRare();
  this.current = liste._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 intimem Wissen über die zugrunde liegende Sammlung.
  2. Die Kopie der Sammlung ist eine Flachkopie.

Dies ist natürlich in keiner Weise thread-sicher. Wie oben erwähnt wurde, ist es ungünstig, die Sammlung während der Iteration zu ändern.

Aber was ist mit dem Problem, dass die Sammlung während der Iteration durch Mittel außerhalb unseres Zugriffs ungültig wird? Bewährte Verfahren empfehlen, die Sammlung während der Operationen und Iterationen zu versionieren und Versionen zu überprüfen, um festzustellen, wann sich die zugrunde liegende Sammlung ändert.

Hier wird es wirklich schwierig. Laut der Microsoft-Dokumentation:

Wenn Änderungen an der Sammlung vorgenommen werden, wie das Hinzufügen, Modifizieren oder Löschen von Elementen, ist das Verhalten des Enumerators undefiniert.

Nun, was bedeutet das? Zum Beispiel bedeutet nur weil List eine Ausnahmebehandlung implementiert, nicht, dass alle Sammlungen, die IList implementieren, dasselbe tun werden. Das scheint eine klare Verletzung des Liskovschen Substitutionsprinzips zu sein:

Objekte einer Oberklasse sollen durch Objekte ihrer Unterklassen ersetzt werden können, ohne die Anwendung zu zerstören.

Ein weiteres Problem ist, dass der Enumerator IDisposable implementieren muss -- das bedeutet eine weitere potenzielle Ursache für Speicherlecks, nicht nur wenn der Aufrufer etwas falsch macht, sondern auch wenn der Autor das Dispose-Muster nicht korrekt implementiert.

Zu guter Letzt haben wir ein Lebensdauerproblem... was passiert, wenn der Iterator gültig ist, aber die zugrunde liegende Sammlung nicht vorhanden ist? Wir haben jetzt einen Schnappschuss von dem, was war... wenn Sie die Lebensdauer einer Sammlung und ihrer Iteratoren trennen, bitten Sie um Ärger.

Lassen Sie uns jetzt ForEach(x => { }) betrachten:

namen.ForEach(name =>
{

});

Dies erweitert sich zu:

public void ForEach(Action aktion)
{
  if (aktion == 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)
    aktion(this._items[index]);
  if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
    return;
  ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}

Von besonderer Bedeutung ist folgendes:

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

Dieser Code allokiert keine Enumerator (keine Notwendigkeit zum Dispose), und unterbricht nicht während der Iteration.

Beachten Sie, dass hier auch eine Flachkopie der zugrunde liegenden Sammlung durchgeführt wird, aber die Sammlung ist jetzt ein Momentaufnahme. Wenn der Autor nicht korrekt eine Prüfung auf Änderung der Sammlung oder deren Veraltetwerden implementiert, ist die Momentaufnahme immer noch gültig.

Das schützt Sie keinesfalls vor dem Problem der Lebensdauerprobleme... wenn die zugrunde liegende Sammlung verschwindet, haben Sie jetzt eine Flachkopie, die auf das zeigt, was war... aber zumindest haben Sie keine Dispose-Probleme bei verwaisten Iteratoren zu lösen...

Ja, ich habe Iteratoren gesagt... manchmal ist es vorteilhaft, Zustand zu haben. Vielleicht möchten Sie etwas in der Art eines Datenbank-Cursors beibehalten... vielleicht sind mehrere foreach-Stil Iterator's der Weg. Persönlich mag ich diesen Designstil nicht, da es zu viele Lebensdauerprobleme gibt und man auf das Wohlwollen der Autoren der Sammlungen angewiesen ist, auf die man angewiesen ist (es sei denn, man schreibt wirklich alles selbst von Grund auf neu).

Es gibt immer eine dritte Option...

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

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

Es ist Ihre Entscheidung, aber jetzt wissen Sie Bescheid und können informiert entscheiden.

14voto

Ich denke, der someList.ForEach()-Aufruf könnte leicht parallelisiert werden, während die normale foreach-Schleife nicht so einfach parallel ausgeführt werden kann. Es wäre einfach, verschiedene Delegaten auf verschiedenen Kernen auszuführen, was mit einer normalen foreach-Schleife nicht so einfach ist.
Nur meine persönliche Meinung

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X