Коллекция была изменена; операция перечисления может не выполняться

Этот вопрос задают много раз на этом форуме. Я знаю решение проблемы. Но мне любопытно узнать, почему "Операция перечисления не может выполняться при изменении коллекции"

        List<string> list = new List<string>();

        list.Add("a");

        list.Add("b");

        int[] array = new int[6] { 1, 2, 3, 4, 5, 5 };

        HashSet<int> hashSet = new HashSet<int>();

        int i = 0;

        foreach (string s in list)
        {
            list[i] = "test";

            i++;
        }

Но когда я меняю список на list.toarray, он работает.

Ответ 1

Microsoft указывает, что любой объект, который реализует iEnumerable, должен аннулировать любые существующие перечисления при изменении объекта. Общая причина этого требования заключается в том, что для многих типов коллекций трудно гарантировать, что перечислитель будет вести себя разумно, когда коллекция будет изменена. Например, предположим, что List содержит пять значений (A, B, C, D, E), а счетчик работает, устанавливая n в число элементов, а затем выдает элемент (0), элемент (1) и т.д. До элемент (п-1). Если, хотя перечислитель является перечисляющим элементом (2) [C], элемент BB вводится после элемента (1) [т.е. B], перечислитель может затем перейти к выходному элементу (3) [который снова является C,], затем элементу 4 [D], а затем решить его, поскольку он выводит все пять элементов. Это было бы плохой ситуацией (один элемент появился дважды, а один пропал без вести).

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

  • Любой элемент, который существует на протяжении всего перечисления, должен быть возвращен ровно один раз.
  • Любой элемент, который существует для части продолжительности перечисления, должен быть возвращен ровно один раз или вообще не существует, но нет требования относительно того, какие такие элементы (если они есть) возвращаются.
  • Последовательность, в которой возвращаются элементы, должна быть допустимой последовательностью для перечислителя (например, SortedList должен возвращать элементы в отсортированной последовательности; если элемент добавляется в список, который предшествует элементу с уже выводом, этот элемент должен не следует перечислять).
  • В целях (1) и (2) удаляемый элемент считается отличным от того, который добавлен, даже если ключи идентичны; если элемент ключа изменен, элемент перед изменением считается отличным от элемента после.
  • Повторяющиеся перечисления через Reset не гарантируют возврата тех же элементов.

Коллекция VB6-стиля, похоже, соответствует семантике выше, но ни одна из других стандартных коллекций не делает. Слишком плохо - такие требования могут быть удовлетворительно удовлетворены некоторыми структурами данных, но этого будет достаточно, чтобы избежать дублирования данных списка, что часто необходимо в соответствии с действующими правилами.

Ответ 2

В целом коллекции .Net не поддерживают перечисление и изменение в одно и то же время. Строка list[i] = "test" изменяет коллекцию list, пока вы находитесь в середине перечисления ее и, следовательно, генерируется исключение. Правда, это тривиальная модификация и не влияет на структуру списка, но List<T> рассматривает даже изменения места как разрушительные.

Версия ToArray работает, потому что теперь у вас есть 2 коллекции

  • list
  • Созданный массив

На самом деле вы перечисляете массив и, следовательно, модифицируете оригинал list.

Ответ 3

Когда вы делаете list.ToArray() в своем foreach, вы делаете полную копию содержимого списка и перечисляете копию. Вы больше не вносите изменения в список при перечислении по списку.

Выполнение этого действия:

foreach (string s in list.ToArray())
{
   list[i] = "test";
   i++;
}

фактически то же самое, что и при этом:

string[] newArray = list.ToArray();
foreach (string s in newArray)
{
   list[i] = "test";
   i++;
}

Обратите внимание, что во втором случае вы не перечисляете коллекцию, которую вы модифицируете, а скорее совершенно новую коллекцию.