System.InvalidOperationException:la raccolta è stata modificata; l'operazione di enumerazione potrebbe non essere eseguita

System.InvalidOperationException:la raccolta è stata modificata; l'operazione di enumerazione potrebbe non essere eseguita

Se provi ad aggiungere/rimuovere elementi da una raccolta mentre è in esecuzione in un ciclo foreach (enumerato), otterrai la seguente eccezione:

Questo errore può verificarsi in due scenari:

  • Stai scorrendo la raccolta in un ciclo foreach e la modifichi (aggiungi/rimuovendo) nello stesso ciclo.
  • Hai una race condition:stai scorrendo la raccolta in un thread mentre un altro thread sta modificando la raccolta.

La soluzione a questo problema dipende dallo scenario in cui ti trovi. In questo articolo esaminerò questi scenari e le possibili soluzioni.

Scenario 1 – La raccolta viene modificata nel ciclo foreach

Questo scenario è molto comune. Di solito gli sviluppatori si imbattono in questo quando tentano di rimuovere elementi da una raccolta, come questo:

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

Ciò genererà InvalidOperationException in fase di esecuzione. Secondo me, sarebbe meglio se il compilatore rilevasse questo problema e mostrasse invece un errore in fase di compilazione.

La soluzione è assicurarsi di non modificare la raccolta nel ciclo foreach.

Soluzione 1:se stai rimuovendo elementi, usa RemoveAll()

Se stai modificando la raccolta rimuovendo elementi, la soluzione più semplice è utilizzare invece LINQ RemoveAll(), in questo modo:

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

Questo rimuove gli elementi che soddisfano le condizioni e non genera l'eccezione di runtime.

Soluzione 2:se stai aggiungendo elementi, mettili in una temporanea e usa AddRange()

Dal momento che non puoi aggiungere elementi mentre lo esegui in un ciclo foreach, la soluzione più semplice è salvare l'elenco di elementi che desideri aggiungere in un elenco temporaneo, quindi utilizzare AddRange(), in questo modo:

var itemsToAdd = new List<string>();

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

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

Soluzione 3:usa un ciclo for regolare e un ciclo al contrario

Invece di usare un ciclo foreach, puoi usare un ciclo for normale. Quando modifichi una raccolta in un ciclo, è una buona idea eseguire il ciclo al contrario. Ecco un esempio di ciclo inverso e aggiunta di elementi:

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

Se provassi la stessa logica mentre esegui il ciclo in avanti, risulterebbe effettivamente in un ciclo infinito.

Scenario 2:un thread sta modificando la raccolta mentre un altro thread sta scorrendo su di esso

Quando si verifica l'eccezione di runtime e sai che il ciclo foreach non sta modificando la raccolta e il tuo codice è multithread, allora ci sono buone probabilità che tu abbia una race condition.

Il codice seguente mostra un esempio di questo scenario:

//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)

Questo codice non è thread-safe. Un thread sta modificando la raccolta mentre un altro thread sta scorrendo su di esso. Il thread che esegue il loop verrà eseguito in InvalidOperationException. Poiché si tratta di una condizione di gara, l'errore non si verifica ogni volta, il che significa che è possibile che questo bug entri in produzione. I bug del multithreading sono così subdoli.

Ogni volta che esegui il multithreading, devi controllare l'accesso alle risorse condivise. Un modo per farlo è usare le serrature. Un modo migliore per farlo in questo scenario è utilizzare una raccolta simultanea.

Soluzione:utilizza una raccolta simultanea

La commutazione del campo movieCollection in ConcurrentBag elimina la race condition.

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() non risolve il problema e genera un'eccezione diversa

Se hai una condizione di gara, l'uso di ToList() non risolverà il problema. In effetti, le condizioni di gara ci saranno ancora, sarà solo un'eccezione diversa.

Ecco un esempio di tentativo di utilizzare ToList() per provare a correggere la race condition originale:

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

Alla fine si verificherà la seguente eccezione:

Ciò è causato da una condizione di razza. Un thread sta chiamando ToList() e un altro thread sta modificando l'elenco. Qualunque cosa ToList() stia facendo internamente, non è thread-safe.

Non utilizzare ToList(). Utilizza invece una raccolta simultanea.