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

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

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

7 ответов

Решение

Из IEnumerable документации:

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

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

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

Как

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

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

Зачем

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

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

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

Например..

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

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

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

Обратите внимание, что изменение элемента - это прекрасно, только Add/Remove - нет.

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

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

Это указанное поведение:

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

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

Другие вопросы по тегам