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

90

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

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 для бесконечного повторяющегося универсального списка )

SWeko
источник
7
Согласен. Это - сомнительно. Я хочу, чтобы вы разместили это в Microsoft Connect и попросили разъяснений.
TomTom
4
«Для меня это стало неожиданностью, поскольку я всегда думал, что List.ForEach был либо просто оболочкой для, foreachлибо для for». Его еще можно было использовать for. Вы можете выполнить то же действие в forцикле и в результате сгенерировать такое же исключение OutOfMemoryException.
Энтони Пеграм
Это основано на моем вопросе: stackoverflow.com/q/9311272/132239 , спасибо SWeko за подробности
Касрак

Ответы:

68

Это потому, что 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 нужно просто запустить один раз для моего примера. Он явно как-то освежился.
SWeko
7
Он обновляется в Add method -> this._items [this._size ++] = item;
Фабио
1
@SWeko, не рассчитывается, обновляется каждый раз, когда элемент добавляется или удаляется.
Thomas Levesque
1
В нем есть _versionчастная переменная, List<T>которая может обнаруживать такие сценарии, поскольку она обновляется при операциях, которые изменяют сам список.
SWeko
Вы можете избежать исключений, сначала получив размер (int theSize = this._size), а затем используя его в цикле for?
Lazlow
14

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

Алексей Рага
источник
6

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

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

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

Майк Перрено
источник
И чтобы ответить на комментарий к сообщению @Thomas о том, как он обновляется - внутренние члены обновляются при вызове add, поэтому он может идти в ногу с изменениями. Если бы вы выполняли вставку с индексом меньше текущего, вы бы никогда не работали с этим элементом, потому что он уже прошел итерацию после этого элемента. Но так как вы добавляете в конец, это работает.
Майк Перрено,
1
Да, при замене Addстроки strings.Insert(0, s + "!")просто выводится «образец». Странно, что об этом вообще не упоминается в документации.
SWeko
Что ж, я думаю, что Microsoft осознала, что практически невозможно предоставить все оговорки, существующие в их документации, поэтому они предоставляют свой исходный код сейчас. Честно говоря, я считаю, что это лучшее решение, но единственная проблема, которую я обнаружил, заключается в том, что такие продукты, как WF, не обновляются так быстро - исходный код 4.x WF все еще недоступен.
Майк Перрено,
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 Team)

Дэвид Кин
источник
1
Я вижу, что это было бы большим плохим переломом, но, тем не менее, оно может потерпеть неудачу неочевидным образом, а это никогда не бывает хорошо. Я не вижу сценария, в котором использование метода ForEach превосходит простое for (или foreach, если изменение исходного списка не требуется)
SWeko