1152 Stimmen

Sammlung wurde geändert; Aufzählungsoperation darf nicht ausgeführt werden

Ich kann diesem Fehler nicht auf den Grund gehen, denn wenn der Debugger angeschlossen ist, scheint er nicht aufzutreten.

Sammlung wurde geändert; Aufzählungsoperation darf nicht ausgeführt werden

Nachstehend finden Sie den Code.

Dies ist ein WCF-Server in einem Windows-Dienst. Die Methode NotifySubscribers() wird vom Dienst immer dann aufgerufen, wenn ein Datenereignis vorliegt (in zufälligen Abständen, aber nicht sehr oft - etwa 800 Mal pro Tag).

Wenn ein Windows Forms-Client ein Abonnement abschließt, wird die Abonnenten-ID zum Abonnentenverzeichnis hinzugefügt, und wenn der Client das Abonnement abbricht, wird sie aus dem Verzeichnis gelöscht. Der Fehler tritt auf, wenn (oder nachdem) ein Client sich abmeldet. Es scheint, dass das nächste Mal, wenn der NotifySubscribers() Methode aufgerufen wird, wird die foreach() Schleife schlägt mit dem Fehler in der Betreffzeile fehl. Die Methode schreibt den Fehler in das Anwendungsprotokoll, wie im folgenden Code gezeigt. Wenn ein Debugger angeschlossen ist und ein Kunde sich abmeldet, wird der Code ordnungsgemäß ausgeführt.

Sehen Sie ein Problem mit diesem Code? Muss ich das Wörterbuch thread-sicher machen?

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class SubscriptionServer : ISubscriptionServer
{
    private static IDictionary<Guid, Subscriber> subscribers;

    public SubscriptionServer()
    {            
        subscribers = new Dictionary<Guid, Subscriber>();
    }

    public void NotifySubscribers(DataRecord sr)
    {
        foreach(Subscriber s in subscribers.Values)
        {
            try
            {
                s.Callback.SignalData(sr);
            }
            catch (Exception e)
            {
                DCS.WriteToApplicationLog(e.Message, 
                  System.Diagnostics.EventLogEntryType.Error);

                UnsubscribeEvent(s.ClientId);
            }
        }
    }

    public Guid SubscribeEvent(string clientDescription)
    {
        Subscriber subscriber = new Subscriber();
        subscriber.Callback = OperationContext.Current.
                GetCallbackChannel<IDCSCallback>();

        subscribers.Add(subscriber.ClientId, subscriber);

        return subscriber.ClientId;
    }

    public void UnsubscribeEvent(Guid clientId)
    {
        try
        {
            subscribers.Remove(clientId);
        }
        catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                    e.Message);
        }
    }
}

0 Stimmen

In meinem Fall war es ein Kollateraleffekt, weil ich einige .Include("table") verwendete, die während des Prozesses geändert wurden - nicht sehr offensichtlich, wenn man den Code liest. Ich hatte jedoch Glück, dass diese Includes nicht benötigt wurden (yeah! alter, nicht gepflegter Code) und ich mein Problem löste, indem ich sie einfach entfernte

0 Stimmen

Bitte sehen Sie sich die Antwort von @joe an. Das ist in vielen Fällen eine viel bessere Lösung. stackoverflow.com/a/57799537/10307728

2030voto

JaredPar Punkte 699699

Was wahrscheinlich passiert ist, dass SignalData während der Schleife indirekt das Abonnentenwörterbuch ändert und zu dieser Meldung führt. Sie können dies überprüfen, indem Sie das

foreach(Subscriber s in subscribers.Values)

An

foreach(Subscriber s in subscribers.Values.ToList())

Wenn ich Recht habe, wird das Problem verschwinden.

Aufruf von subscribers.Values.ToList() kopiert die Werte von subscribers.Values in eine separate Liste am Anfang der foreach . Nichts anderes hat Zugriff auf diese Liste (sie hat nicht einmal einen Variablennamen!), so dass sie innerhalb der Schleife nicht verändert werden kann.

17 Stimmen

BTW .ToList() ist in System.Core dll vorhanden, die nicht mit .NET 2.0-Anwendungen kompatibel ist. Sie müssen also möglicherweise Ihre Zielanwendung auf .Net 3.5 ändern.

5 Stimmen

Das ist großartig. Ich tat es mit einer ArrayList und es funktionierte auch perfekt (natürlich mit ToArray())

69 Stimmen

Ich verstehe nicht, warum Sie eine ToList erstellt haben und warum das alles behebt

133voto

Mitch Wheat Punkte 287474

Wenn ein Abonnent sich abmeldet, ändern Sie den Inhalt der Abonnentensammlung während der Aufzählung.

Es gibt mehrere Möglichkeiten, dies zu beheben. Eine davon besteht darin, die for-Schleife so zu ändern, dass sie eine explizite .ToList() :

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...

80voto

Effizienter ist es meiner Meinung nach, eine weitere Liste zu erstellen, in die Sie alles eintragen, was "entfernt werden soll". Nachdem Sie Ihre Hauptschleife beendet haben (ohne .ToList()), führen Sie eine weitere Schleife über die "zu entfernende" Liste durch und entfernen jeden Eintrag, sobald er auftritt. In Ihrer Klasse fügen Sie also hinzu:

private List<Guid> toBeRemoved = new List<Guid>();

Dann ändern Sie es in:

public void NotifySubscribers(DataRecord sr)
{
    toBeRemoved.Clear();

    ...your unchanged code skipped...

   foreach ( Guid clientId in toBeRemoved )
   {
        try
        {
            subscribers.Remove(clientId);
        }
        catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                e.Message);
        }
   }
}

...your unchanged code skipped...

public void UnsubscribeEvent(Guid clientId)
{
    toBeRemoved.Add( clientId );
}

Dies löst nicht nur Ihr Problem, sondern verhindert auch, dass Sie immer wieder eine Liste aus Ihrem Wörterbuch erstellen müssen, was bei einer großen Anzahl von Abonnenten teuer ist. Vorausgesetzt, die Liste der zu entfernenden Abonnenten ist bei jeder Iteration kleiner als die Gesamtzahl der Abonnenten in der Liste, sollte dies schneller gehen. Aber Sie können natürlich gerne ein Profil erstellen, um sicherzugehen, dass dies in Ihrer speziellen Situation der Fall ist.

13 Stimmen

Ich gehe davon aus, dass dies eine Überlegung wert ist, wenn Sie mit größeren Sammlungen arbeiten. Wenn es klein würde ich wahrscheinlich nur ToList und weitergehen.

1 Stimmen

Dies ist die bessere und leistungsfähigere Option. Das Kopieren der meisten Auflistungstypen ist unter der Haube wirklich leichtgewichtig, und tolist() kann auf die gleiche Weise wie oben brechen, es ist nur weniger wahrscheinlich, was es zu einer schwierigeren [ ]

60voto

Shri Punkte 2332

Warum dieser Fehler?

Im Allgemeinen unterstützen .Net-Sammlungen nicht die gleichzeitige Aufzählung und Änderung. Wenn Sie versuchen, die Sammlungsliste während der Aufzählung zu ändern, löst dies eine Ausnahme aus. Das Problem, das sich hinter diesem Fehler verbirgt, ist also, dass wir die Liste/das Wörterbuch nicht ändern können, während wir eine Schleife durch dieselbe ziehen.

Eine der Lösungen

Wenn wir ein Wörterbuch anhand einer Liste seiner Schlüssel durchlaufen, können wir parallel dazu das Wörterbuchobjekt ändern, da wir durch die Schlüsselsammlung und nicht durch das Wörterbuch iterieren und nicht das Wörterbuch (und dessen Schlüsselsammlung) durchlaufen.

Beispiel

//get key collection from dictionary into a list to loop through
List<int> keys = new List<int>(Dictionary.Keys);

// iterating key collection using a simple for-each loop
foreach (int key in keys)
{
  // Now we can perform any modification with values of the dictionary.
  Dictionary[key] = Dictionary[key] - 1;
}

Hier ist ein Blogbeitrag über diese Lösung.

Und für einen tieferen Einblick in StackOverflow: Warum tritt dieser Fehler auf?

0 Stimmen

Ab .NET Core 3.0 mit C# 8.0 kann eine das Wörterbuch kann während der Aufzählung (foreach) nur über .Remove und .Clear geändert werden . Dies gilt nicht für andere Sammlungen.

0 Stimmen

Ich verstehe nicht, wie sich der Fehler dadurch vermeiden lässt. Ändern Sie am Ende nicht immer noch Dictionary[key]?

10voto

Mark Aven Punkte 165

Okay, was mir geholfen hat, war, rückwärts zu iterieren. Ich habe versucht, einen Eintrag aus einer Liste zu entfernen, aber die Iteration nach oben und es vermasselt die Schleife, weil der Eintrag nicht mehr existiert:

for (int x = myList.Count - 1; x > -1; x--)
{
    myList.RemoveAt(x);
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X