web-dev-qa-db-fra.com

La collection a été modifiée. l'opération d'énumération peut ne pas s'exécuter

Je ne peux pas aller au fond de cette erreur, car lorsque le débogueur est attaché, cela ne semble pas se produire. Ci-dessous le code.

Ceci est un serveur WCF dans un service Windows. La méthode NotifySubscribers est appelée par le service chaque fois qu'il y a un événement de données (à intervalles aléatoires, mais pas très souvent - environ 800 fois par jour).

Lorsqu'un client Windows Forms s'abonne, l'ID d'abonné est ajouté au dictionnaire des abonnés et, lorsque le client se désabonne, il est supprimé du dictionnaire. L'erreur se produit lorsqu'un client se désabonne (ou après). Il semble que la prochaine fois que la méthode NotifySubscribers () soit appelée, la boucle foreach () échoue avec l'erreur dans la ligne de sujet. La méthode écrit l'erreur dans le journal de l'application, comme indiqué dans le code ci-dessous. Lorsqu'un débogueur est attaché et qu'un client se désabonne, le code s'exécute correctement.

Voyez-vous un problème avec ce code? Dois-je rendre le dictionnaire thread-safe?

[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);
        }
    }
}
760
cdonner

Il est probable que SignalData modifie indirectement le dictionnaire des abonnés sous le capot pendant la boucle et entraîne ce message. Vous pouvez le vérifier en changeant 

foreach(Subscriber s in subscribers.Values)

À 

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

Si j'ai raison, le problème disparaîtra

L'appel de subscribers.Values.ToList () copie les valeurs de subscribers.Values ​​dans une liste distincte au début de foreach. Rien d'autre n'a accès à cette liste (elle n'a même pas de nom de variable!), Donc rien ne peut la modifier à l'intérieur de la boucle.

1382
JaredPar

Lorsqu'un abonné se désabonne, vous modifiez le contenu de la collection d'abonnés lors de l'énumération.

Il existe plusieurs façons de résoudre ce problème, l'une étant de changer la boucle for pour utiliser un .ToList() explicite:

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

Un moyen plus efficace, à mon avis, est d’avoir une autre liste dans laquelle vous déclarez que vous insérez tout ce qui doit être "supprimé". Ensuite, lorsque vous avez terminé votre boucle principale (sans le .ToList ()), vous effectuez une autre boucle sur la liste "à supprimer", en supprimant chaque entrée au fur et à mesure. Donc, dans votre classe, vous ajoutez:

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

Ensuite, vous le changez en:

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 );
}

Cela résoudra non seulement votre problème, mais vous évitera de devoir continuer à créer une liste à partir de votre dictionnaire, ce qui coûte cher s'il y a beaucoup d'abonnés. En supposant que la liste des abonnés à supprimer sur une itération donnée soit inférieure au nombre total dans la liste, cela devrait être plus rapide. Mais bien sûr, n'hésitez pas à le décrire pour vous assurer que c'est le cas en cas de doute sur votre situation d'utilisation spécifique.

55
x4000

Vous pouvez également verrouiller votre dictionnaire des abonnés pour l'empêcher d'être modifié à chaque boucle:

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }
39

Pourquoi cette erreur?

En général, les collections .Net ne peuvent pas être énumérées et modifiées en même temps. Si vous essayez de modifier la liste de collections lors de l'énumération, une exception est levée. Le problème derrière cette erreur est donc que nous ne pouvons pas modifier la liste/le dictionnaire tant que nous parcourons la même chose. 

Une des solutions

Si nous itérons un dictionnaire en utilisant une liste de ses clés, nous pouvons parallèlement modifier l'objet dictionnaire, car nous itérons à travers la collection de clés et Pas le dictionnaire (et itérons sa collection de clés).

Exemple

//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;
}

Voici un article blog à propos de cette solution.

Et pour une plongée profonde dans StackOverflow: Pourquoi cette erreur se produit?

12
open and free

En fait, le problème me semble que vous supprimez des éléments de la liste et que vous vous attendez à continuer de lire la liste comme si de rien n'était.

Ce que vous devez vraiment faire est de commencer par la fin et de revenir au début. Même si vous supprimez des éléments de la liste, vous pourrez continuer à la lire.

4
luc.rg.roy

InvalidOperationException - Une exception InvalidOperationException s'est produite. Il rapporte qu'une "collection a été modifiée" dans une boucle foreach

Utilisez l'instruction break, une fois que l'objet est supprimé.

ex: 

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}
3
vivek

J'ai vu beaucoup d'options pour cela mais pour moi celui-ci était le meilleur.

ListItemCollection collection = new ListItemCollection();
        foreach (ListItem item in ListBox1.Items)
        {
            if (item.Selected)
                collection.Add(item);
        }

Ensuite, parcourez simplement la collection. 

Sachez qu'une ListItemCollection peut contenir des doublons. Par défaut, rien n'empêche l'ajout de doublons à la collection. Pour éviter les doublons, vous pouvez faire ceci:

ListItemCollection collection = new ListItemCollection();
            foreach (ListItem item in ListBox1.Items)
            {
                if (item.Selected && !collection.Contains(item))
                    collection.Add(item);
            }
2
Mike

J'ai eu le même problème, et il a été résolu lorsque j'ai utilisé une boucle for au lieu de foreach.

// foreach (var item in itemsToBeLast)
for (int i = 0; i < itemsToBeLast.Count; i++)
{
    var matchingItem = itemsToBeLast.FirstOrDefault(item => item.Detach);

   if (matchingItem != null)
   {
      itemsToBeLast.Remove(matchingItem);
      continue;
   }
   allItems.Add(itemsToBeLast[i]);// (attachDetachItem);
}
2
Daniel Moreshet

Bon alors ce qui m'a aidé était de revenir en arrière. J'essayais de supprimer une entrée d'une liste, mais je l'ai réorientée vers le haut et cela a gâché la boucle car l'entrée n'existait plus: 

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

                            myList.RemoveAt(x);

                        }
1
Mark Aven

Vous pouvez copier un objet dictionnaire abonné sur un même type d’objet dictionnaire temporaire, puis itérer l’objet dictionnaire temporaire à l’aide de la boucle foreach. 

0
Rezoan

Il y a un lien qui explique très bien la solution et donne également la solution… Essayez-la si vous avez la solution appropriée, postez-la ici afin que les autres puissent comprendre… La solution proposée est correcte, tout comme la publication afin que les autres puissent l'essayer. .

pour vous référence lien d'origine: -  https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

Lorsque nous utilisons des classes .Net Serialization pour sérialiser un objet dont la définition contient un type Enumerable, c'est-à-dire une collection , Vous obtiendrez facilement InvalidOperationException en disant que "La collection a été modifiée; l'opération d'énumération ne peut pas s'exécuter", où votre codage est Dans le cas de scénarios multi-threads, la cause principale est que les classes de sérialisation parcourent une collection via un énumérateur. Par conséquent, le problème consiste à essayer de parcourir une collection en la modifiant.

Première solution, nous pouvons simplement utiliser lock comme solution de synchronisation pour nous assurer que L'opération sur l'objet List ne peut être exécutée qu'à partir d'un thread à la fois . De toute évidence, vous obtiendrez une pénalité de performance. Si vous souhaitez sérialiser une collection de cet objet, le verrou sera appliqué à chacun d’eux.

Eh bien, .Net 4.0, qui facilite la gestion de scénarios multi-threading. Pour ce problème de champ de collection de sérialisation, j'ai découvert que nous pouvions simplement tirer avantage de la classe ConcurrentQueue (Check MSDN), , qui est une collection thread-safe et FIFO et rend le code sans verrouillage.

En utilisant cette classe, dans sa simplicité, ce que vous devez modifier pour votre code remplace le type Collection par ce mot, Utilisez Enqueue pour ajouter un élément à la fin de ConcurrentQueue, supprimez ce code de verrouillage. .__ Ou, si le scénario sur lequel vous travaillez nécessite des éléments de collection tels que List, vous aurez besoin de quelques codes supplémentaires pour adapter ConcurrentQueue à vos champs.

BTW, ConcurrentQueue n’a pas de méthode Clear en raison de l’algorithme sous-jacent qui ne permet pas l’effacement atomique de la collection. Donc, vous devez le faire vous-même, le moyen le plus rapide est de recréer un nouveau ConcurrentQueue vide pour un remplacement.

0
Meghraj

Donc, une autre façon de résoudre ce problème serait au lieu de supprimer les éléments, créer un nouveau dictionnaire et ajouter uniquement les éléments que vous ne vouliez pas supprimer, puis remplacer le dictionnaire d'origine par le nouveau. Je ne pense pas qu'il s'agisse d'un problème d'efficacité car cela n'augmente pas le nombre de fois que vous parcourez la structure.

0
ford prefect