La colección fue modificada; operación de enumeración no se puede ejecutar, ¿por qué?

Estoy enumerando una colección que implementa IList, y durante la enumeración estoy modificando la colección. Me sale el error, “La colección fue modificada, la operación de enumeración puede no ejecutarse”.

Quiero saber por qué ocurre este error al modificar un elemento en la colección durante la iteración. Ya he convertido mi ciclo foreach a un ciclo for, pero quiero saber los ‘detalles’ sobre por qué ocurre este error.

De la documentación de IEnumerable :

Un enumerador sigue siendo válido siempre que la colección permanezca sin cambios. Si se realizan cambios en la colección, como agregar, modificar o eliminar elementos, el enumerador queda irrevocablemente invalidado y su comportamiento no está definido.

Creo que el razonamiento de esta decisión es que no se puede garantizar que todos los tipos de colecciones puedan mantener la modificación y aún así conservar un estado del enumerador. Considere una lista vinculada: si elimina un nodo y un enumerador está actualmente en ese nodo, la referencia del nodo puede ser su único estado. Y una vez que se elimine ese nodo, la referencia del “próximo nodo” se establecerá en null , invalidando efectivamente el estado del enumerador e impidiendo una mayor enumeración.

Como algunas implementaciones de colecciones tendrían serios problemas con este tipo de situación, se decidió hacer que esta parte del contrato de interfaz IEnumerable. Permitir modificaciones en algunas situaciones y otras no sería terriblemente confuso. Además, esto significaría que el código existente que podría depender de la modificación de una colección al enumerarlo tendría serios problemas cuando se cambia la implementación de la colección. Por lo tanto, es preferible hacer el comportamiento consistente en todos los enumerables.

Cómo

Internamente, la mayoría de las clases de colección base mantienen un número de versión. Cada vez que agregue, elimine, reordene, etc. la colección, este número de versión se incrementa.

Cuando comienzas a enumerar, se toma una instantánea del número de versión. Cada vez que se pasa el ciclo, este número de versión se compara con el de la colección, y si son diferentes, se lanza esta excepción.

Por qué

Si bien sería posible implementar IList para que pueda abordar correctamente los cambios en la recostackción realizada dentro del ciclo foreach (haciendo que el enumerador rastree los cambios de la colección), es una tarea mucho más difícil tratar correctamente los cambios realizados en la colección por otros hilos durante la enumeración. De modo que esta excepción existe para ayudar a identificar vulnerabilidades en su código y para proporcionar algún tipo de advertencia temprana sobre cualquier inestabilidad potencial provocada por la manipulación de otros hilos.

Mientras que otros han descrito por qué es un comportamiento no válido, no han ofrecido una solución a su problema. Si bien es posible que no necesite una solución, la voy a proporcionar de todos modos.

Si desea observar una colección que es diferente a la colección sobre la que está iterando, debe devolver una nueva colección.

Por ejemplo..

 IEnumerable sequence = Enumerable.Range(0, 30); IEnumerable newSequence = new List(); foreach (var item in sequence) { if (item < 20) newSequence.Add(item); } // now work with newSequence 

Así es como debería estar 'modificando' las colecciones. LINQ toma este enfoque cuando quiere modificar una secuencia. Por ejemplo:

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

Supongo que la razón es que el estado del objeto enumerador está relacionado con el estado de la colección. Por ejemplo, un enumerador de lista tendría un campo int para almacenar el índice del elemento actual. Sin embargo, si elimina un elemento de la lista, está cambiando todos los índices después de que el elemento se haya ido. En este punto, el enumerador omitiría un objeto, lo que manifestaría un comportamiento incorrecto. Hacer que el enumerador sea válido para todos los casos posibles requeriría una lógica compleja y podría perjudicar el rendimiento para el caso más común (sin cambiar la colección). Creo que esta es la razón por la cual los diseñadores de las colecciones en .NET decidieron que deberían arrojar una excepción cuando el enumerador está en estado de invasión en lugar de tratar de arreglarlo.

La razón técnica real en este escenario es porque las Listas contienen un miembro privado llamado “versión”. Cada modificación – Agregar / Eliminar – incrementa la Versión. El enumerador que devuelve GetEnumerator almacena la versión en el momento en que se crea y verifica la versión cada vez que se llama a Next; si no es igual, arroja una excepción.

Esto es cierto para la clase incluida en la List y posiblemente para otras colecciones, por lo que si implementa su propio IList (en lugar de solo crear una subclasificación / usar una colección integrada internamente), entonces podrá evitarlo, pero en general, La enumeración y la moficación se deben realizar en un bucle forzado hacia atrás o utilizando una lista secundaria, según el escenario.

Tenga en cuenta que modificar un elemento está perfectamente bien, solo Agregar / Eliminar no lo está.

Este es el comportamiento especificado :

Los enumeradores se pueden usar para leer los datos en la colección, pero no se pueden usar para modificar la colección subyacente.

Esta limitación hace que los enumeradores sean más simples de implementar.
Tenga en cuenta que algunas colecciones le permitirán (incorrectamente) enumerar mientras modifica la colección.

La razón por la que ve esto es que el IEnumerator devuelto por la colección subyacente puede exponer la propiedad actual como de solo lectura. En general, debe evitar cambios en las colecciones (de hecho, la mayoría de los casos ni siquiera podrá cambiar la colección) usando for-each.