Почему List <T>.ForEach позволяет изменять его список? - PullRequest
91 голосов
/ 16 февраля 2012

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

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.
У кого-нибудь есть объяснение, как и почему это поведение?

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

Ответы [ 4 ]

68 голосов
/ 16 февраля 2012

Это потому, что метод 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 увеличивается на каждой итерации.

14 голосов
/ 16 февраля 2012

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

6 голосов
/ 16 февраля 2012

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

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

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

4 голосов
/ 25 февраля 2012

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

        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)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...