Wie sie sagen, der Teufel steckt im Detail ...
Der größte Unterschied zwischen den beiden Methoden der Aufzählung von Sammlungen besteht darin, dass foreach
der 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 foreach
zuerst 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:
- Wir erhalten ein zustandsbehaftetes Objekt mit vertrauter Kenntnis der zugrunde liegenden Sammlung zurück.
- 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 IDisposable
bedeutet, dass eine andere Quelle potenzieller Speicherlecks auftritt, nicht nur, wenn der Aufrufer etwas falsch macht, sondern wenn der Autor das Dispose
Muster 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 Dispose
Problem 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 foreach
Stile 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.