Подтвердить что ты не робот

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

Я перечисляю коллекцию, которая реализует IList, и во время перечисления я изменяю коллекцию. Я получаю сообщение об ошибке: "Коллекция была изменена, операция перечисления не может выполняться".

Я хочу знать, почему эта ошибка возникает при изменении элемента в коллекции во время итерации. Я уже преобразовал цикл foreach в цикл for, но я хочу знать "подробности" о том, почему эта ошибка возникает.

4b9b3361

Ответ 1

Из IEnumerable documentation:

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

Я считаю, что аргументация для этого решения заключается в том, что не может быть гарантировано, что все типы коллекций могут поддерживать модификацию и сохранять состояние перечислителя. Рассмотрите связанный список - если вы удалите node, и в настоящее время перечислитель на нем node, ссылка node может быть его единственным состоянием. И как только этот node будет удален, ссылка "next node" будет установлена ​​на null, что фактически приведет к аннулированию состояния перечислителя и предотвращению дальнейшего перечисления.

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

Ответ 2

Как

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

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

Почему

В то время как было бы возможно реализовать IList, чтобы он мог корректно обрабатывать изменения в коллекции, сделанные в цикле foreach (с учетом того, что перечислитель отслеживает изменения коллекции), задача гораздо сложнее справиться с изменения, внесенные в коллекцию другими потоками во время перечисления. Таким образом, это исключение существует, чтобы помочь выявить уязвимости в вашем коде и обеспечить раннее предупреждение о любых потенциальных неустойчивостях, вызванных манипуляциями с другими потоками.

Ответ 3

В то время как другие описали, почему это недействительное поведение, они не предложили решение вашей проблемы. Хотя вам может и не понадобиться решение, я все равно его предоставит.

Если вы хотите наблюдать коллекцию, которая отличается от коллекции, которую вы повторяете, вы должны вернуть новую коллекцию.

Например..

IEnumerable<int> sequence = Enumerable.Range(0, 30);
IEnumerable<int> newSequence = new List<int>();

foreach (var item in sequence) {
    if (item < 20) newSequence.Add(item);
}

// now work with newSequence

Вот как вы должны "изменять" коллекции. LINQ использует этот подход, если вы хотите изменить последовательность. Например:

var newSequence = sequence.Where(item => item < 20); // returns new sequence

Ответ 4

Я предполагаю, что причина в том, что состояние объекта перечислителя связано с состоянием коллекции. Например, перечислитель списка должен иметь int-поле для хранения индекса текущего элемента. Однако, если вы удаляете элемент из списка, вы перемещаете все индексы после того, как элемент останется на одном. В этот момент перечислитель пропускает объект, тем самым проявляя неправильное поведение. Сделать перечислитель действительным для всех возможных случаев потребует сложной логики и может повредить производительность для наиболее распространенного случая (не меняя коллекцию). Я считаю, поэтому дизайнеры коллекций в .NET решили, что они должны просто выбросить исключение, когда перечислитель находится в состоянии invald, а не пытается его исправить.

Ответ 5

Реальная техническая причина в этом сценарии состоит в том, что списки содержат частный член, называемый "версия". Каждая модификация - Добавить/Удалить - увеличивает версию. Перечислитель, возвращающий GetEnumerator, сохраняет версию в момент ее создания и проверяет версию каждый раз, когда вызывается "Далее" - если она не равна, она выдает исключение.

Это справедливо для встроенного класса List<T> и, возможно, для других коллекций, поэтому, если вы реализуете свой собственный IList (а не просто подклассифицируете/используете встроенную коллекцию внутри), тогда вы сможете обойти это, но как правило, перечисление и mofication должны выполняться в обратном для цикла или с использованием вторичного списка в зависимости от сценария.

Обратите внимание, что изменение элемента отлично, только добавление/удаление не выполняется.

Ответ 6

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

Ответ 7

Это указано:

Перечислители могут использоваться для чтения данных в коллекции, но они не могут использоваться для изменения базовая коллекция.

Это ограничение упрощает внедрение счетчиков. Обратите внимание, что некоторые коллекции (некорректно) позволят вам перечислять при изменении коллекции.