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:
- Wir erhalten ein zustandsbehaftetes Objekt mit intimem Wissen über die zugrunde liegende Sammlung.
- 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.