Почему List <T>.ForEach позволяет изменить его список?

Если я использую:

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

Add в foreach вызывает исключение InvalidOperationException (коллекция была изменена, операция перечисления может не выполняться), что я считаю логичным, так как мы вытаскиваем коврик из-под ног.

Однако, если я использую:

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

он быстро стреляет в ногу, зацикливая до тех пор, пока не выкинет исключение OutOfMemoryException.

Это для меня как сюрприз, поскольку я всегда думал, что List.ForEach был либо просто оберткой для foreach, либо для for.
Кто-нибудь объясняет, как и почему это поведение?

(Inpired by цикл forEach для универсального списка повторяется бесконечно)

Ответ 1

Это потому, что метод ForEach не использует перечислитель, он перемещается по элементам с помощью цикла for:

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

(код, полученный с помощью JustDecompile)

Поскольку счетчик не используется, он никогда не проверяет, изменился ли список, и конечное условие цикла for никогда не достигается, потому что _size увеличивается на каждой итерации.

Ответ 2

List<T>.ForEach реализуется через for внутри, поэтому он не использует перечислитель и позволяет изменять коллекцию.

Ответ 3

Поскольку объект ForEach, подключенный к классу List, внутренне использует цикл for, который напрямую привязан к его внутренним членам, что вы можете увидеть, загрузив исходный код для платформы .NET.

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

Где в качестве цикла foreach в первую очередь оптимизация компилятора, но также должна действовать против коллекции как наблюдателя, поэтому, если коллекция модифицирована, она генерирует исключение.

Ответ 4

Мы знаем об этой проблеме, это был надзор, когда она была изначально написана. К сожалению, мы не можем изменить его, потому что теперь это предотвратит запуск этого ранее работающего кода:

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

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

Полезность этого метода сама по себе сомнительна, поскольку Эрик Липперт указал, поэтому мы не включили его в .NET для приложений стиля Metro (например, приложения Windows 8).

Дэвид Кин (команда BCL)