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

911

Je ne peux pas aller au fond de cette erreur, car lorsque le débogueur est connecté, il ne semble pas se produire. Voici le code.

Il s'agit d'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 lorsque (ou après) un client se désabonne. 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 des applications 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 sûr pour les threads?

[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);
        }
    }
}
cdonner
la source
dans mon cas, c'était un effet collatéral car j'utilisais quelques .Include ("table") qui ont été modifiés au cours du processus - pas très évidents lors de la lecture du code. mais j'ai eu la chance que les Comprend ne sont pas nécessaires (oui vieux, le code unmaintained!) et je résolus mon problème simplement en les supprimant
Adi

Réponses:

1632

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

foreach(Subscriber s in subscribers.Values)

À

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

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

L'appel subscribers.Values.ToList()copie les valeurs de subscribers.Valuesdans 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.

JaredPar
la source
14
BTW .ToList () est présent dans la DLL System.Core qui n'est pas compatible avec les applications .NET 2.0. Vous devrez donc peut-être changer votre application cible en .Net 3.5
mishal153
60
Je ne comprends pas pourquoi vous avez fait une ToList et pourquoi cela corrige tout
PositiveGuy
204
@CoffeeAddict: Le problème est qu'il subscribers.Valuesest en cours de modification à l'intérieur de la foreachboucle. L'appel subscribers.Values.ToList()copie les valeurs de subscribers.Valuesdans 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.
BlueRaja - Danny Pflughoeft
31
Notez que cela ToListpeut également se produire si la collection a été modifiée pendant l' ToListexécution.
Sriram Sakthivel
16
Je suis sûr que penser ne résout pas le problème, mais le rend simplement plus difficile à reproduire. ToListn'est pas une opération atomique. Ce qui est encore plus drôle, ToListbasiquement, fait son propre foreachinterne pour copier les éléments dans une nouvelle instance de liste, ce qui signifie que vous avez résolu un foreachproblème en ajoutant une foreachitération supplémentaire (bien que plus rapide) .
Groo
113

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

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

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...
Blé Mitch
la source
64

Un moyen plus efficace, à mon avis, est d'avoir une autre liste dans laquelle vous déclarez que vous mettez tout ce qui est "à supprimer". Ensuite, après avoir terminé votre boucle principale (sans le .ToList ()), vous effectuez une autre boucle sur la liste «à supprimer», 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 d'avoir à créer une liste à partir de votre dictionnaire, ce qui est coûteux s'il y a beaucoup d'abonnés. 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 s'il y a un doute dans votre situation d'utilisation spécifique.

x4000
la source
9
Je m'attends à ce que cela vaille la peine d'être considéré si vous en avez un, vous travaillez avec de plus grandes collections. Si c'était petit, je devrais probablement juste ToList et passer à autre chose.
Karl Kieninger
42

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

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }
Mohammad Sepahvand
la source
2
Est-ce un exemple complet? J'ai une classe (_dictionary obj ci-dessous) qui contient un dictionnaire générique <string, int> nommé MarkerFrequencies, mais cela n'a pas résolu instantanément le plantage: lock (_dictionary.MarkerFrequencies) {foreach (KeyValuePair <string, int> paire dans _dictionary.MarkerFrequencies) {...}}
Jon Coombs
4
@JCoombs, il est possible que vous modifiiez, éventuellement réaffectiez le MarkerFrequenciesdictionnaire à l'intérieur du verrou lui-même, ce qui signifie que l'instance d'origine n'est plus verrouillée. Essayez également d'utiliser un forau lieu d'un foreach, voir ceci et cela . Faites-moi savoir si cela résout le problème.
Mohammad Sepahvand le
1
Eh bien, j'ai finalement réussi à corriger ce bogue, et ces conseils ont été utiles - merci! Mon jeu de données était petit, et cette opération n'était qu'une mise à jour de l'affichage de l'interface utilisateur, il était donc préférable d'itérer sur une copie. (J'ai également expérimenté l'utilisation de for au lieu de foreach après avoir verrouillé MarkerFrequencies dans les deux threads. Cela a empêché le plantage mais semblait nécessiter un travail de débogage supplémentaire. Et cela a introduit plus de complexité. Ma seule raison d'avoir deux threads ici est de permettre à l'utilisateur d'annuler une opération, d'ailleurs. L'interface utilisateur ne modifie pas directement ces données.)
Jon Coombs
9
Le problème est que, pour les grandes applications, les verrous peuvent être un problème majeur de performances - il vaut mieux utiliser une collection dans l' System.Collections.Concurrentespace de noms.
BrainSlugs83
28

Pourquoi cette erreur?

En général, les collections .Net ne prennent pas en charge l'énumération et la modification en même temps. Si vous essayez de modifier la liste de collection pendant l'énumération, elle déclenche une exception. Donc, le problème derrière cette erreur est que nous ne pouvons pas modifier la liste / dictionnaire pendant que nous parcourons la même chose.

Une des solutions

Si nous itérons un dictionnaire en utilisant une liste de ses clés, en parallèle, nous pouvons modifier l'objet dictionnaire, car nous parcourons la collection de clés et non 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 de blog sur cette solution.

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

ouvert et gratuit
la source
Je vous remercie! Une explication claire de pourquoi cela se produit et m'a immédiatement donné le moyen de résoudre ce problème dans mon application.
Kim Crosser
5

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

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 à la lire.

luc.rg.roy
la source
Je ne vois pas comment cela ferait une différence? Si vous supprimez un élément au milieu de la liste, il générera toujours une erreur car l'élément auquel il essaie d'accéder est introuvable?
Zapnologica
4
@Zapnologica la différence est - vous n'énuméreriez pas la liste - au lieu de faire un pour / chaque, vous feriez un pour / suivant et y accéder par un entier - vous pouvez certainement modifier une liste a dans un pour / prochaine boucle, mais jamais dans une boucle pour / chaque (car pour / chaque énumère ) - vous pouvez également le faire aller de l'avant dans un pour / suivant, à condition d'avoir une logique supplémentaire pour ajuster vos compteurs, etc.
BrainSlugs83
4

InvalidOperationException - Une exception InvalidOperationException s'est produite. Il signale 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;
    }
}
vivek
la source
Bonne et simple solution si vous savez que votre liste contient au plus un élément à supprimer.
Tawab Wakil
3

J'ai eu le même problème, et il a été résolu lorsque j'ai utilisé une forboucle à la place 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);
}
Daniel Moreshet
la source
11
Ce code est incorrect et ignorera certains éléments de la collection si un élément est supprimé. Par exemple: vous avez var arr = ["a", "b", "c"] et dans la première itération (i = 0) vous supprimez l'élément à la position 0 (élément "a"). Après cela, tous les éléments du tableau se déplaceront d'une position vers le haut et le tableau sera ["b", "c"]. Donc, dans la prochaine itération (i = 1), vous vérifierez l'élément à la position 1 qui sera "c" et non "b". C'est faux. Pour résoudre ce problème, vous devez vous déplacer de bas en haut
Kaspars Ozols
3

D'accord, ce qui m'a aidé à répéter. J'essayais de supprimer une entrée d'une liste mais en itérant vers le haut et cela a foiré la boucle parce que l'entrée n'existait plus:

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

                            myList.RemoveAt(x);

                        }
Mark Aven
la source
2

J'ai vu de nombreuses options pour cela, mais pour moi, celle-ci était la meilleure.

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

Parcourez ensuite simplement la collection.

N'oubliez pas qu'un ListItemCollection peut contenir des doublons. Par défaut, rien n'empêche l'ajout de doublons à la collection. Pour éviter les doublons, vous pouvez procéder comme suit:

ListItemCollection collection = new ListItemCollection();
            foreach (ListItem item in ListBox1.Items)
            {
                if (item.Selected && !collection.Contains(item))
                    collection.Add(item);
            }
Mike
la source
Comment ce code empêcherait-il l'entrée de doublons dans la base de données. J'ai écrit quelque chose de similaire et lorsque j'ajoute de nouveaux utilisateurs à partir de la liste, si je garde accidentellement un sélectionné qui est déjà dans la liste, cela créera une entrée en double. Avez-vous des suggestions @Mike?
Jamie
1

La réponse acceptée est imprécise et incorrecte dans le pire des cas. Si des modifications sont apportées pendantToList() , vous pouvez toujours vous retrouver avec une erreur. En outre lock, quelles sont les performances et la sécurité des threads à prendre en compte si vous avez un membre public, une solution appropriée peut utiliser des types immuables .

En général, un type immuable signifie que vous ne pouvez pas en changer l'état une fois créé. Votre code devrait donc ressembler à:

public class SubscriptionServer : ISubscriptionServer
{
    private static ImmutableDictionary<Guid, Subscriber> subscribers = ImmutableDictionary<Guid, Subscriber>.Empty;
    public void SubscribeEvent(string id)
    {
        subscribers = subscribers.Add(Guid.NewGuid(), new Subscriber());
    }
    public void NotifyEvent()
    {
        foreach(var sub in subscribers.Values)
        {
            //.....This is always safe
        }
    }
    //.........
}

Cela peut être particulièrement utile si vous avez un membre public. Les autres classes peuvent toujours foreachsur les types immuables sans se soucier de la modification de la collection.

Joe
la source
1

Voici un scénario spécifique qui justifie une approche spécialisée:

  1. Le Dictionaryest fréquemment énuméré.
  2. Le Dictionaryest rarement modifié.

Dans ce scénario, la création d'une copie du Dictionary(ou du Dictionary.Values) avant chaque énumération peut être assez coûteuse. Mon idée sur la résolution de ce problème est de réutiliser la même copie mise en cache dans plusieurs énumérations et de regarder un IEnumeratorde l'original Dictionarypour les exceptions. L'énumérateur sera mis en cache avec les données copiées et interrogé avant de commencer une nouvelle énumération. En cas d'exception, la copie mise en cache sera supprimée et une nouvelle sera créée. Voici ma mise en œuvre de cette idée:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

public class EnumerableSnapshot<T> : IEnumerable<T>, IDisposable
{
    private IEnumerable<T> _source;
    private IEnumerator<T> _enumerator;
    private ReadOnlyCollection<T> _cached;

    public EnumerableSnapshot(IEnumerable<T> source)
    {
        _source = source ?? throw new ArgumentNullException(nameof(source));
    }

    public IEnumerator<T> GetEnumerator()
    {
        if (_source == null) throw new ObjectDisposedException(this.GetType().Name);
        if (_enumerator == null)
        {
            _enumerator = _source.GetEnumerator();
            _cached = new ReadOnlyCollection<T>(_source.ToArray());
        }
        else
        {
            var modified = false;
            if (_source is ICollection collection) // C# 7 syntax
            {
                modified = _cached.Count != collection.Count;
            }
            if (!modified)
            {
                try
                {
                    _enumerator.MoveNext();
                }
                catch (InvalidOperationException)
                {
                    modified = true;
                }
            }
            if (modified)
            {
                _enumerator.Dispose();
                _enumerator = _source.GetEnumerator();
                _cached = new ReadOnlyCollection<T>(_source.ToArray());
            }
        }
        return _cached.GetEnumerator();
    }

    public void Dispose()
    {
        _enumerator?.Dispose();
        _enumerator = null;
        _cached = null;
        _source = null;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public static class EnumerableSnapshotExtensions
{
    public static EnumerableSnapshot<T> ToEnumerableSnapshot<T>(
        this IEnumerable<T> source) => new EnumerableSnapshot<T>(source);
}

Exemple d'utilisation:

private static IDictionary<Guid, Subscriber> _subscribers;
private static EnumerableSnapshot<Subscriber> _subscribersSnapshot;

//...(in the constructor)
_subscribers = new Dictionary<Guid, Subscriber>();
_subscribersSnapshot = _subscribers.Values.ToEnumerableSnapshot();

// ...(elsewere)
foreach (var subscriber in _subscribersSnapshot)
{
    //...
}

Malheureusement, cette idée ne peut pas être utilisée actuellement avec la classe Dictionarydans .NET Core 3.0, car cette classe ne renvoie pas d' exception Collection a été modifiée lors de l'énumération et des méthodes Removeet Clearsont invoquées. Tous les autres conteneurs que j'ai vérifiés se comportent de manière cohérente. J'ai vérifié systématiquement ces classes: List<T>, Collection<T>, ObservableCollection<T>, HashSet<T>, SortedSet<T>, Dictionary<T,V>et SortedDictionary<T,V>. Seules les deux méthodes susmentionnées de la Dictionaryclasse dans .NET Core n'invalident pas l'énumération.


Mise à jour: j'ai résolu le problème ci-dessus en comparant également les longueurs de la collection mise en cache et de la collection d'origine. Ce correctif suppose que le dictionnaire sera transmis directement comme argument pour le EnumerableSnapshotconstructeur de », et son identité ne sera pas caché par (par exemple) une projection comme: dictionary.Select(e => e).ΤοEnumerableSnapshot().


Important: La classe ci-dessus n'est pas thread-safe. Il est destiné à être utilisé à partir de code s'exécutant exclusivement dans un seul thread.

Theodor Zoulias
la source
0

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

Rezoan
la source
(Ce message ne semble pas fournir une réponse de qualité à la question. Veuillez modifier votre réponse et, ou simplement le poster en tant que commentaire à la question).
sɐunıɔ ןɐ qɐp
0

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

préfet de ford
la source
0

Il y a un lien où il a très bien élaboré et une solution est également donnée. Essayez-le si vous avez la bonne solution, veuillez poster ici afin que d'autres puissent comprendre. La solution donnée est ok alors comme le poste afin que d'autres puissent essayer ces solutions.

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

Lorsque nous utilisons des classes de sérialisation .Net pour sérialiser un objet dont sa définition contient un type Enumerable, c'est-à-dire une collection, vous obtiendrez facilement InvalidOperationException disant "La collection a été modifiée; l'opération d'énumération peut ne pas s'exécuter" lorsque votre codage est dans des scénarios multi-threads. La cause inférieure est que les classes de sérialisation vont parcourir la collection via l'énumérateur, en tant que tel, le problème va essayer d'itérer à travers une collection tout 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. Évidemment, vous obtiendrez une pénalité de performance si vous voulez sérialiser une collection de cet objet, alors pour chacun d'eux, le verrou sera appliqué.

Eh bien, .Net 4.0 qui rend le traitement des scénarios multi-threading très pratique. pour ce problème de champ de collection de sérialisation, j'ai trouvé que nous pouvions simplement profiter 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é, les éléments que vous devez modifier pour votre code remplacent le type Collection par celui-ci, utilisez Enqueue pour ajouter un élément à la fin de ConcurrentQueue, supprimez ces codes de verrouillage. Ou, si le scénario sur lequel vous travaillez nécessite des éléments de collecte tels que List, vous aurez besoin d'un peu plus de code 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. vous devez donc le faire vous-même, le moyen le plus rapide est de recréer un nouveau ConcurrentQueue vide pour un remplacement.

user8851697
la source
-1

J'ai personnellement rencontré cela récemment dans un code inconnu où il se trouvait dans un dbcontext ouvert avec .Remove (item) de Linq. J'ai eu énormément de temps à localiser le débogage d'erreur parce que les éléments avaient été supprimés la première fois, et si j'essayais de prendre du recul et de recommencer, la collection était vide, car j'étais dans le même contexte!

Michael Sefranek
la source