Если я использую:
var strings = new List<string> { "sample" };
foreach (string s in strings)
{
Console.WriteLine(s);
strings.Add(s + "!");
}
the Add
в foreach
выдает InvalidOperationException (Коллекция была изменена; операция перечисления может не выполняться), что я считаю логичным, поскольку мы вытаскиваем коврик из-под ног.
Однако, если я использую:
var strings = new List<string> { "sample" };
strings.ForEach(s =>
{
Console.WriteLine(s);
strings.Add(s + "!");
});
он быстро стреляет себе в ногу, зацикливаясь, пока не выдаст OutOfMemoryException.
Для меня это стало неожиданностью, так как я всегда думал, что List.ForEach был либо просто оболочкой для, foreach
либо для for
.
Есть ли у кого-нибудь объяснение того, как и почему такое поведение?
(Вдохновленный циклом ForEach для бесконечного повторяющегося универсального списка )
foreach
либо дляfor
». Его еще можно было использоватьfor
. Вы можете выполнить то же действие вfor
цикле и в результате сгенерировать такое же исключение OutOfMemoryException.Ответы:
Это потому, что
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
оно увеличивается на каждой итерации.источник
_size
рассчитывается? Если это просто предварительно рассчитано, тогда if нужно просто запустить один раз для моего примера. Он явно как-то освежился._version
частная переменная,List<T>
которая может обнаруживать такие сценарии, поскольку она обновляется при операциях, которые изменяют сам список.List<T>.ForEach
реализованfor
внутри, поэтому он не использует перечислитель и позволяет изменять коллекцию.источник
Поскольку ForEach, прикрепленный к классу List, внутренне использует цикл for, который напрямую присоединен к его внутренним членам, что вы можете увидеть, загрузив исходный код для .NET framework.
http://referencesource.microsoft.com/netframework.aspx
Поскольку цикл foreach является, прежде всего, оптимизацией компилятора, но он также должен работать с коллекцией как наблюдатель, поэтому, если коллекция изменяется, она генерирует исключение.
источник
Add
строкиstrings.Insert(0, s + "!")
просто выводится «образец». Странно, что об этом вообще не упоминается в документации.Мы знаем об этой проблеме, когда она изначально писалась, это была недоработка. К сожалению, мы не можем его изменить, потому что теперь это предотвратит запуск этого ранее работающего кода:
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 Team)
источник