Почему 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)