System.InvalidOperationException:se modificó la colección; la operación de enumeración puede no ejecutarse

System.InvalidOperationException:se modificó la colección; la operación de enumeración puede no ejecutarse

Si intenta agregar/eliminar elementos de una colección mientras se repite en un bucle foreach (enumerado), obtendrá la siguiente excepción:

Este error puede ocurrir en dos escenarios:

  • Está recorriendo la colección en un bucle foreach y modificándola (agregar/eliminar) en el mismo bucle.
  • Tiene una condición de carrera:está recorriendo la colección en un subproceso mientras otro subproceso está modificando la colección.

La solución a este problema depende del escenario en el que se encuentre. En este artículo, repasaré estos escenarios y las posibles soluciones.

Escenario 1:la colección se modifica en el bucle foreach

Este escenario es muy común. Por lo general, los desarrolladores se encontrarán con esto cuando intenten eliminar elementos de una colección, como este:

foreach (var movie in movieCollection)
{
	if (movie.Contains(removeMovie))
	{
		movieCollection.Remove(removeMovie);
	}
}
Code language: C# (cs)

Esto arrojará InvalidOperationException en tiempo de ejecución. En mi opinión, sería mejor si el compilador detectara este problema y mostrara un error de tiempo de compilación en su lugar.

La solución es asegurarse de no modificar la colección en el ciclo foreach.

Solución 1:si está eliminando elementos, utilice RemoveAll()

Si está modificando la colección eliminando elementos, entonces la solución más simple es usar LINQ RemoveAll() en su lugar, así:

movieCollection.RemoveAll(movie => movie.Contains(removeMovie));
Code language: C# (cs)

Esto elimina los elementos que cumplen las condiciones y no lanza la excepción de tiempo de ejecución.

Solución 2:si está agregando elementos, colóquelos en una temperatura y use AddRange()

Dado que no puede agregar elementos mientras realiza un bucle foreach, la solución más simple es guardar la lista de elementos que desea agregar en una lista temporal y luego usar AddRange(), así:

var itemsToAdd = new List<string>();

foreach (var movie in movieCollection)
{
	if (movie.Contains(duplicateMovie))
	{
		itemsToAdd.Add(duplicateMovie);
	}
}

movieCollection.AddRange(itemsToAdd);
Code language: C# (cs)

Solución 3:use un bucle for normal y un bucle inverso

En lugar de usar un bucle foreach, puede usar un bucle for regular. Cuando está modificando una colección en un bucle, es una buena idea hacerlo al revés. Este es un ejemplo de bucle inverso y adición de elementos:

for (int i = movieCollection.Count - 1; i >= 0; i--)
{
	if (movieCollection[i].Contains(duplicateMovie))
	{
		movieCollection.Add(duplicateMovie);
	}
}
Code language: C# (cs)

Si intentara la misma lógica mientras realiza un bucle hacia adelante, en realidad resultaría en un bucle infinito.

Escenario 2:un subproceso está modificando la colección mientras que otro subproceso lo recorre

Cuando ocurre la excepción en tiempo de ejecución y sabe que el ciclo foreach no está modificando la recopilación, y su código es de subprocesos múltiples, entonces existe una buena posibilidad de que tenga una condición de carrera.

El siguiente código muestra un ejemplo de este escenario:

//Resource shared between multiple threads (recipe for a race condition)
private List<string> movieCollection = new List<string>();

//Called by thread 1
void Post(string movie)
{
	movieCollection.Add(movie);
}

//Called by thread 2
void GetAll()
{
        //Race condition results in InvalidOperationException (can't modify collection while enumerating) here
	foreach (var movie in movieCollection)
	{
		Console.WriteLine(movie);
	}
}
Code language: C# (cs)

Este código no es seguro para subprocesos. Un subproceso está modificando la colección mientras que otro subproceso lo recorre. El subproceso que está en bucle se encontrará con InvalidOperationException. Dado que esta es una condición de carrera, el error no ocurrirá siempre, lo que significa que es posible que este error llegue a producción. Los errores de subprocesos múltiples son así de astutos.

Cada vez que utiliza subprocesos múltiples, necesita controlar el acceso a los recursos compartidos. Una forma de hacerlo es usar candados. Una mejor manera de hacerlo en este escenario es usar una colección concurrente.

Solución:usar una colección concurrente

Cambiar el campo movieCollection para que sea ConcurrentBag elimina la condición de carrera.

using System.Collections.Concurrent;

private ConcurrentBag<string> movieCollection = new ConcurrentBag<string>();

//Called by thread 1
void Post(string movie)
{
	movieCollection.Add(movie);
}

//Called by thread 2
void GetAll()
{
	foreach (var movie in movieCollection)
	{
		Console.WriteLine(movie);
	}
}
Code language: C# (cs)

ToList() no resuelve el problema y da como resultado una excepción diferente

Si tiene una condición de carrera, usar ToList() no resolverá el problema. De hecho, la condición de carrera seguirá ahí, solo será una excepción diferente.

Aquí hay un ejemplo de intentar usar ToList() para tratar de corregir la condición de carrera original:

void GetAll()
{
	var snapshot = movieCollection.ToList();
	foreach (var movie in snapshot)
	{
		Console.WriteLine(movie);
	}
}
Code language: C# (cs)

Eventualmente, esto se encontrará con la siguiente excepción:

Esto es causado por una condición de carrera. Un hilo está llamando a ToList() y otro hilo está modificando la lista. Independientemente de lo que esté haciendo ToList() internamente, no es seguro para subprocesos.

No use ToList(). Utilice una colección concurrente en su lugar.