Чому List <T> .ForEach дозволяє змінювати його список?


90

Якщо я використовую:

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


7
Я згоден. Це - сумнівно. Я бажаю вам розмістити це на Microsoft Connect і попросити пояснень.
TomTom

4
"Це для мене несподіванка, оскільки я завжди думав, що List.ForEach був просто обгорткою для foreachабо для for". Він все ще міг би використовувати for. Ви можете виконати ту саму дію в forциклі і в результаті створити той самий OutOfMemoryException.
Ентоні Пеграм,

Це засновано на моє запитання: stackoverflow.com/q/9311272/132239 , спасибі SWeko за потрапляння в це деталі
Kasrak

Відповіді:


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розраховується? Якщо це просто попередньо розраховано, тоді якщо для мого прикладу потрібно просто запустити один раз. Очевидно, це якось оновилось.
SWeko

7
Він оновлюється за допомогою методу Додати -> this._items [this._size ++] = item;
Фабіо

1
@SWeko, він не обчислюється, він оновлюється кожного разу, коли елемент додається або видаляється.
Томас Левеск,

1
Існує _versionприватна змінна, List<T>яка може виявити подібні сценарії, оскільки вона оновлюється для операцій, що змінюють сам список.
SWeko

Ви можете уникнути винятків, спершу отримавши розмір (int theSize = this._size), а потім використовуючи його в циклі for?
Лазлов

14

List<T>.ForEachреалізовано через forinside, тому він не використовує перечислювач і дозволяє змінювати колекцію.


6

Оскільки ForEach, приєднаний до класу List, внутрішньо використовує цикл for, який безпосередньо приєднаний до його внутрішніх членів, - що ви можете побачити, завантаживши вихідний код для .NET framework.

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

Якщо цикл foreach - це, перш за все, оптимізація компілятора, але він також повинен діяти проти колекції як спостерігач - тож, якщо колекція модифікується, вона видає виняток.


А щоб відповісти на коментар у дописі @Thomas про те, як він оновлюється - внутрішні члени оновлюються, коли викликається add, тому саме він може йти в ногу зі змінами. Якби ви виконували вставку з індексом, меншим за поточний, ви б ніколи не оперували цим елементом, оскільки він вже повторювався за цим елементом. Але оскільки ви додаєте до кінця, це працює.
Mike Perrenoud

1
Так, зміна Addрядка strings.Insert(0, s + "!")просто роздруковує "зразок". Дивно, що про це взагалі не йдеться у документації.
SWeko

Ну, я думаю, Microsoft зрозуміла, що практично неможливо надати всі застереження, які існують у їхній документації - тому вони надають свій вихідний код зараз. Я вважаю, що найкраще рішення чесно, але єдина проблема, яку я виявив, полягає в тому, що такі продукти, як WF, оновлюються не так швидко - вихідний код 4.x WF все ще недоступний.
Mike Perrenoud

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)


1
Я бачу, що це була б велика погана зламана зміна, але тим не менше вона може зазнати невдалих наслідків, і це ніколи не є хорошою справою. Я не бачу сценарію, коли використання методу ForEach перевершує простий для (або foreach, якщо не потрібно
маніпулювання
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.