System.InvalidOperationException:Sammlung wurde geändert; Enumerationsoperation wird möglicherweise nicht ausgeführt

System.InvalidOperationException:Sammlung wurde geändert; Enumerationsoperation wird möglicherweise nicht ausgeführt

Wenn Sie versuchen, Elemente zu einer Sammlung hinzuzufügen/zu entfernen, während sie in einer foreach-Schleife (aufgezählt) durchlaufen wird, erhalten Sie die folgende Ausnahme:

Dieser Fehler kann in zwei Szenarien auftreten:

  • Sie durchlaufen die Sammlung in einer foreach-Schleife und ändern sie (hinzufügen/entfernen) in derselben Schleife.
  • Sie haben eine Race-Bedingung:Sie durchlaufen die Sammlung in einem Thread, während ein anderer Thread die Sammlung ändert.

Die Lösung für dieses Problem hängt davon ab, in welchem ​​Szenario Sie sich befinden. In diesem Artikel gehe ich auf diese Szenarien und mögliche Lösungen ein.

Szenario 1 – Die Sammlung wird in der foreach-Schleife geändert

Dieses Szenario ist sehr verbreitet. Normalerweise stoßen Entwickler darauf, wenn sie versuchen, Elemente aus einer Sammlung zu entfernen, wie hier:

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

Dadurch wird zur Laufzeit InvalidOperationException ausgelöst. Meiner Meinung nach wäre es besser, wenn der Compiler dieses Problem erkennen und stattdessen einen Kompilierungsfehler anzeigen würde.

Die Lösung besteht darin, sicherzustellen, dass Sie die Sammlung in der foreach-Schleife nicht ändern.

Lösung 1 – Wenn Sie Elemente entfernen, verwenden Sie RemoveAll()

Wenn Sie die Sammlung ändern, indem Sie Elemente entfernen, ist die einfachste Lösung, stattdessen LINQ RemoveAll() zu verwenden, wie hier:

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

Dadurch werden Elemente entfernt, die die Bedingungen erfüllen, und es wird keine Laufzeitausnahme ausgelöst.

Lösung 2 – Wenn Sie Elemente hinzufügen, fügen Sie sie in eine temporäre Datei ein und verwenden Sie AddRange()

Da Sie keine Elemente hinzufügen können, während Sie sie in einer foreach-Schleife durchlaufen, besteht die einfachste Lösung darin, die Liste der hinzuzufügenden Elemente in einer temporären Liste zu speichern und dann AddRange() wie folgt zu verwenden:

var itemsToAdd = new List<string>();

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

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

Lösung 3 – Verwenden Sie eine reguläre for-Schleife und eine umgekehrte Schleife

Anstatt eine foreach-Schleife zu verwenden, können Sie eine normale for-Schleife verwenden. Wenn Sie eine Sammlung in einer Schleife ändern, empfiehlt es sich, die Schleife rückwärts zu durchlaufen. Hier ist ein Beispiel für eine Rückwärtsschleife und das Hinzufügen von Elementen:

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

Wenn Sie die gleiche Logik beim Vorwärtsschleifen ausprobieren würden, würde dies tatsächlich zu einer Endlosschleife führen.

Szenario 2 – Ein Thread ändert die Sammlung, während ein anderer Thread sie durchläuft

Wenn die Laufzeitausnahme auftritt und Sie wissen, dass die foreach-Schleife die Sammlung nicht ändert und Ihr Code multithreaded ist, besteht eine gute Chance, dass Sie eine Race-Bedingung haben.

Der folgende Code zeigt ein Beispiel für dieses Szenario:

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

Dieser Code ist nicht Thread-sicher. Ein Thread ändert die Sammlung, während ein anderer Thread sie durchläuft. Der Thread, der sich in einer Schleife befindet, wird in die InvalidOperationException ausgeführt. Da es sich um eine Racebedingung handelt, tritt der Fehler nicht jedes Mal auf, was bedeutet, dass dieser Fehler möglicherweise in die Produktion gelangt. Multithreading-Bugs sind so hinterhältig.

Jedes Mal, wenn Sie Multithreading verwenden, müssen Sie den Zugriff auf freigegebene Ressourcen kontrollieren. Eine Möglichkeit, dies zu tun, ist die Verwendung von Sperren. Ein besserer Weg, dies in diesem Szenario zu tun, ist die Verwendung einer gleichzeitigen Sammlung.

Lösung – Verwenden Sie eine gleichzeitige Sammlung

Durch Ändern des Felds movieCollection in ConcurrentBag wird die Racebedingung beseitigt.

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() löst das Problem nicht und führt zu einer anderen Ausnahme

Wenn Sie eine Race-Condition haben, wird die Verwendung von ToList() das Problem nicht lösen. Tatsächlich wird die Rennbedingung immer noch da sein, es wird nur eine andere Ausnahme sein.

Hier ist ein Beispiel für den Versuch, ToList() zu verwenden, um zu versuchen, die ursprüngliche Racebedingung zu beheben:

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

Schließlich wird dies auf die folgende Ausnahme stoßen:

Dies wird durch eine Racebedingung verursacht. Ein Thread ruft ToList() auf und ein anderer Thread modifiziert die Liste. Was auch immer ToList() intern tut, es ist nicht Thread-sicher.

Verwenden Sie ToList() nicht. Verwenden Sie stattdessen eine gleichzeitige Sammlung.