Warum erlaubt List <T> .ForEach, dass die Liste geändert wird?

90

Wenn ich benutze:

var strings = new List<string> { "sample" };
foreach (string s in strings)
{
  Console.WriteLine(s);
  strings.Add(s + "!");
}

Das Addin den foreachWürfen eine InvalidOperationException (Sammlung wurde geändert; Aufzählungsoperation wird möglicherweise nicht ausgeführt), die ich für logisch halte, da wir den Teppich unter unseren Füßen hervorziehen.

Wenn ich jedoch benutze:

var strings = new List<string> { "sample" };
strings.ForEach(s =>
  {
    Console.WriteLine(s);
    strings.Add(s + "!");
  });

Es schießt sich sofort in den Fuß, indem es eine Schleife ausführt, bis eine OutOfMemoryException ausgelöst wird.

Dies ist eine Überraschung für mich, da ich immer dachte, dass List.ForEach entweder nur ein Wrapper für foreachoder für ist for.
Hat jemand eine Erklärung für das Wie und Warum dieses Verhaltens?

(Inspiriert von der ForEach-Schleife für eine endlos wiederholte generische Liste )

SWeko
quelle
7
Genau. Das ist - zweifelhaft. Ich möchte, dass Sie das auf Microsoft Connect posten und um Klarstellung bitten.
TomTom
4
"Das ist eine Überraschung für mich, da ich immer dachte, dass List.ForEach entweder nur ein Wrapper für foreachoder für ist for." Es könnte noch gebrauchen for. Sie können dieselbe Aktion in einer forSchleife ausführen und als Ergebnis dieselbe OutOfMemoryException generieren.
Anthony Pegram
Dies basiert auf meiner Frage: stackoverflow.com/q/9311272/132239 , danke SWeko für die Details
Kasrak

Antworten:

68

Dies liegt daran, dass die ForEachMethode den Enumerator nicht verwendet, sondern die Elemente mit einer forSchleife durchläuft :

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

(Code mit JustDecompile erhalten)

Da der Enumerator nicht verwendet wird, wird nie überprüft, ob sich die Liste geändert hat, und die Endbedingung der forSchleife wird nie erreicht, da sie _sizebei jeder Iteration erhöht wird.

Thomas Levesque
quelle
Ja, aber wie wird _sizeberechnet? Wenn es nur vorberechnet ist, sollte es für mein Beispiel nur einmal ausgeführt werden. Es ist offensichtlich irgendwie erfrischt.
SWeko
7
Es wird aktualisiert unter Add method -> this._items [this._size ++] = item;
Fabio
1
@SWeko, es wird nicht berechnet, es wird jedes Mal aktualisiert, wenn ein Element hinzugefügt oder entfernt wird.
Thomas Levesque
1
Es gibt eine _versionprivate Variable List<T>, die diese Art von Szenarien erkennen kann, da sie bei Vorgängen aktualisiert wird, die die Liste selbst ändern.
SWeko
Sie können Ausnahmen vermeiden, indem Sie zuerst die Größe (int theSize = this._size) abrufen und dann in der for-Schleife verwenden.
Lazlow
14

List<T>.ForEachwird über forinside implementiert , verwendet also keinen Enumerator und ermöglicht das Ändern der Sammlung.

Alexey Raga
quelle
6

Da das an die List-Klasse angehängte ForEach intern eine for-Schleife verwendet, die direkt an die internen Mitglieder angehängt ist. Dies können Sie sehen, indem Sie den Quellcode für das .NET-Framework herunterladen.

http://referencesource.microsoft.com/netframework.aspx

Wobei eine foreach-Schleife in erster Linie eine Compileroptimierung ist, aber auch als Beobachter gegen die Sammlung arbeiten muss. Wenn also die Sammlung geändert wird, wird eine Ausnahme ausgelöst.

Mike Perrenoud
quelle
Und um den Kommentar zu @Thomas Beitrag zu beantworten, wie es aktualisiert wird: Die internen Mitglieder werden aktualisiert, wenn add aufgerufen wird. Deshalb kann es mit den Änderungen Schritt halten. Wenn Sie eine Einfügung an einem Index durchführen würden, der kleiner als der aktuelle ist, würden Sie dieses Element niemals bearbeiten, da es bereits über dieses Element hinaus iteriert wurde. Aber da Sie am Ende hinzufügen, funktioniert es.
Mike Perrenoud
1
Ja, wenn Sie die AddZeile ändern , wird strings.Insert(0, s + "!")nur "Probe" ausgedruckt. Es ist seltsam, dass dies in der Dokumentation überhaupt nicht erwähnt wird.
SWeko
Ich denke, Microsoft hat erkannt, dass es nahezu unmöglich ist, alle in ihrer Dokumentation enthaltenen Einschränkungen bereitzustellen. Daher stellen sie jetzt ihren Quellcode bereit. Ich finde das ehrlich gesagt eine bessere Lösung, aber das einzige Problem, das ich gefunden habe, ist, dass Produkte wie WF nicht so schnell aktualisiert werden - 4.x WF-Quellcode ist immer noch nicht verfügbar.
Mike Perrenoud
4

Wir wissen über dieses Problem Bescheid, es war ein Versehen, als es ursprünglich geschrieben wurde. Leider können wir es nicht ändern, da dies jetzt verhindern würde, dass dieser zuvor funktionierende Code ausgeführt wird:

        var list = new List<string>();
        list.Add("Foo");
        list.Add("Bar");

        list.ForEach((item) => 
        { 
            if(item=="Foo") 
                list.Remove(item); 
        });

Die Nützlichkeit dieser Methode selbst ist fraglich, wie Eric Lippert betonte, weshalb wir sie für .NET für Apps im Metro-Stil (dh Windows 8-Apps) nicht aufgenommen haben.

David Kean (BCL-Team)

David Kean
quelle
1
Ich sehe, dass dies eine große, schlimme Veränderung wäre, aber dennoch kann es auf nicht offensichtliche Weise scheitern, und das ist niemals eine gute Sache. Ich kann kein Szenario sehen, in dem die Verwendung der ForEach-Methode einer einfachen for (oder foreach, wenn das Mangeln der ursprünglichen Liste nicht erforderlich ist)
überlegen