Dlaczego List <T> .ForEach pozwala na modyfikowanie swojej listy?

90

Jeśli używam:

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

Addw foreachrzuca InvalidOperationException (Kolekcja została zmodyfikowana; operacja wyliczania nie może wykonać), które uważam za logiczne, ponieważ jesteśmy pociągając dywanik spod naszych stóp.

Jeśli jednak używam:

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

natychmiast strzela sobie w stopę, wykonując pętlę, aż wyrzuci wyjątek OutOfMemoryException.

Zaskoczyło mnie to, ponieważ zawsze myślałem, że List.ForEach to tylko opakowanie dla foreachlub dla for.
Czy ktoś ma wyjaśnienie, jak i dlaczego to zachowanie?

(Zainspirowany pętlą ForEach dla listy ogólnej powtarzanej w nieskończoność )

SWeko
źródło
7
Zgadzam się. To jest - wątpliwe. Oczekuję, że opublikujesz to na microsoft connect i proszę o wyjaśnienie.
TomTom
4
„To dla mnie niespodzianka, ponieważ zawsze myślałem, że List.ForEach to tylko opakowanie dla foreachlub dla for”. Nadal przydałby się for. Możesz wykonać tę samą akcję w forpętli i wygenerować ten sam wyjątek OutOfMemoryException.
Anthony Pegram
To jest oparte na moim pytaniu: stackoverflow.com/q/9311272/132239 , dzięki SWeko za wejście w szczegóły
Kasrak

Odpowiedzi:

68

Dzieje się tak, ponieważ ForEachmetoda nie używa modułu wyliczającego, przechodzi przez elementy z forpętlą:

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

(kod uzyskany z JustDecompile)

Ponieważ moduł wyliczający nie jest używany, nigdy nie sprawdza, czy lista uległa zmianie, a warunek końcowy forpętli nigdy nie zostanie osiągnięty, ponieważ _sizejest zwiększany przy każdej iteracji.

Thomas Levesque
źródło
Tak, ale jak to jest _sizeobliczane? Jeśli jest to tylko wstępnie obliczone, to należy uruchomić raz dla mojego przykładu. To oczywiście jakoś odświeżone.
SWeko,
7
Jest odświeżany w metodzie Dodaj -> this._items [this._size ++] = item;
Fabio,
1
@SWeko, nie jest obliczane, jest aktualizowane za każdym razem, gdy element jest dodawany lub usuwany.
Thomas Levesque
1
Istnieje _versionzmienna prywatna, List<T>która może wykrywać tego rodzaju scenariusze, ponieważ jest aktualizowana na podstawie operacji, które zmieniają samą listę.
Seko
Możesz uniknąć wyjątków, najpierw pobierając rozmiar (int theSize = this._size), a następnie używając go w pętli for?
Lazlow
14

List<T>.ForEachjest zaimplementowany forwewnątrz, dzięki czemu nie korzysta z modułu wyliczającego i pozwala na modyfikację kolekcji.

Alexey Raga
źródło
6

Ponieważ ForEach dołączona do klasy List wewnętrznie używa pętli for, która jest bezpośrednio dołączona do jej wewnętrznych elementów członkowskich - co można zobaczyć, pobierając kod źródłowy platformy .NET Framework.

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

Gdzie jako pętla foreach jest przede wszystkim optymalizacją kompilatora, ale także musi działać na kolekcji jako obserwator - więc jeśli kolekcja zostanie zmodyfikowana, zgłasza wyjątek.

Mike Perrenoud
źródło
Odpowiadając na komentarz do posta @Thomas o tym, jak się odświeża - członkowie wewnętrzni są odświeżani po wywołaniu add, dzięki czemu jest w stanie nadążyć za zmianami. Jeśli miałbyś wykonać wstawianie, przy indeksie mniejszym niż bieżący, nigdy nie operowałbyś na tym elemencie, ponieważ jest już iterowany po tym elemencie. Ale skoro dodajesz do końca, to działa.
Mike Perrenoud
1
Tak, zmieniając Addwiersz strings.Insert(0, s + "!")po prostu wypisuje „próbkę”. Dziwne, że nie ma o tym w ogóle wzmianki w dokumentacji.
SWeko
Cóż, myślę, że Microsoft zdał sobie sprawę, że prawie niemożliwe jest podanie wszystkich zastrzeżeń, które istnieją w ich dokumentacji - więc udostępniają teraz swój kod źródłowy. Szczerze mówiąc uważam, że jest to lepsze rozwiązanie, ale jedynym problemem, jaki znalazłem, jest to, że produkty takie jak WF nie są aktualizowane tak szybko - kod źródłowy WF 4.x nadal nie jest dostępny.
Mike Perrenoud
4

Wiemy o tym problemie, było to przeoczenie, kiedy zostało pierwotnie napisane. Niestety nie możemy tego zmienić, ponieważ uniemożliwiłoby to teraz uruchomienie tego wcześniej działającego kodu:

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

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

Sama użyteczność tej metody jest wątpliwa, jak zauważył Eric Lippert , więc nie uwzględniliśmy jej dla .NET dla aplikacji w stylu Metro (tj. Aplikacji Windows 8).

David Kean (zespół BCL)

David Kean
źródło
1
Widzę, że byłaby to duża zła, przełomowa zmiana, ale mimo to może się nie powieść w nieoczywisty sposób, a to nigdy nie jest dobre. Nie widzę scenariusza, w którym użycie metody ForEach jest lepsze od prostego for (lub foreach, jeśli zniekształcenie oryginalnej listy nie jest wymagane)
SWeko