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


Je ne peux pas aller au fond de cette erreur, parce que quand le débogueur est attaché, il ne semble pas se produire. Voici le code.

C'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 (à des intervalles aléatoires, mais pas très souvent - environ 800 fois par jour).

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

Voyez-vous un problème avec ce code? Ai-je besoin de 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);
        }
    }
}

694
2018-03-03 02:01


origine


Réponses:


Ce qui se passe probablement, c'est que SignalData modifie indirectement le dictionnaire des abonnés sous le capot pendant la boucle et mène à ce message. Vous pouvez vérifier cela en changeant

foreach(Subscriber s in subscribers.Values)

À

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

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


1275
2018-03-03 02:10



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

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

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

95
2018-03-03 02:13



Une façon plus efficace, à mon avis, est d'avoir une autre liste dans laquelle vous déclarez que vous mettez tout ce qui doit être «retiré». Ensuite, après avoir terminé votre boucle principale (sans le .ToList ()), vous faites une autre boucle sur la liste "à supprimer", en supprimant chaque entrée comme il arrive. 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 ne résoudra pas seulement votre problème, il vous évitera de devoir continuer à créer une liste à partir de votre dictionnaire, ce qui est coûteux s'il y a beaucoup d'abonnés là-bas. En supposant que la liste des abonnés à supprimer sur une itération donnée est inférieure au nombre total dans la liste, cela devrait être plus rapide. Mais bien sûr, n'hésitez pas à le profiler pour être sûr que c'est le cas en cas de doute sur votre situation d'utilisation spécifique.


53
2018-03-03 06:58



Vous pouvez également verrouiller votre dictionnaire d'abonnés pour l'empêcher d'être modifié chaque fois qu'il est en boucle:

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

33
2018-05-23 19:32



Remarque: En général, les collections .Net ne supportent pas d'être énumérées et modifiées en même temps. Si vous essayez de modifier la liste de collection alors que vous êtes en train de l'énumérer, elle déclenchera une exception.

Donc le problème derrière cette erreur est, nous ne pouvons pas modifier la liste / dictionnaire pendant que nous sommes en boucle. Mais si nous itérons un dictionnaire en utilisant une liste temporaire de ses clés, en parallèle nous pouvons modifier l'objet du dictionnaire, car maintenant nous n'éditons pas le dictionnaire (et itéérons sa collection de clés).

échantillon: 

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

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

Voici une article de blogà propos de cette solution.

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


9
2017-11-11 12:12



En fait, le problème me semble que vous supprimez des éléments de la liste et que vous continuez à lire la liste comme si rien ne s'était passé.

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


4
2018-05-23 16:10



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

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

ex:

ArrayList list = new ArrayList(); 

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

3
2018-03-16 07:20



J'ai eu le même problème, et il a été résolu quand j'ai utilisé un for boucle 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
2018-06-16 07:29



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, faites simplement une boucle dans la collection.

Sachez qu'un ListItemCollection peut contenir des doublons. Par défaut, rien n'empêche d'ajouter des doublons à la collection. Pour éviter les doublons, vous pouvez le faire:

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

2
2018-03-04 20:55



D'accord, ce qui m'a aidé était l'itération en arrière. J'essayais de supprimer une entrée d'une liste mais de la remonter et elle a foiré la boucle parce que l'entrée n'existait plus:

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

                            myList.RemoveAt(x);

                        }

1
2017-07-22 14:14