Pregunta La colección fue modificada; la operación de enumeración no se puede ejecutar


No puedo llegar al final de este error, porque cuando el depurador está conectado, parece que no ocurre. A continuación está el código.

Este es un servidor WCF en un servicio de Windows. El servicio llama al método NotifySubscribers cada vez que hay un evento de datos (a intervalos aleatorios, pero no muy a menudo, unas 800 veces al día).

Cuando un cliente de Windows Forms se suscribe, la identificación del suscriptor se agrega al diccionario del suscriptor, y cuando el cliente cancela la suscripción, se elimina del diccionario. El error ocurre cuando (o después) un cliente cancela la suscripción. Parece que la próxima vez que se llame al método NotifySubscribers (), el bucle foreach () falla con el error en la línea de asunto. El método escribe el error en el registro de la aplicación como se muestra en el siguiente código. Cuando se conecta un depurador y un cliente cancela la suscripción, el código se ejecuta correctamente.

¿Ves un problema con este código? ¿Debo hacer que el diccionario sea seguro para subprocesos?

[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


origen


Respuestas:


Lo que probablemente está sucediendo es que SignalData está cambiando indirectamente el diccionario de suscriptores debajo del capó durante el ciclo y conduciendo a ese mensaje. Puede verificar esto cambiando

foreach(Subscriber s in subscribers.Values)

A

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

Si estoy en lo cierto, el problema desaparecerá


1275
2018-03-03 02:10



Cuando un suscriptor cancela la suscripción, está cambiando los contenidos de la colección de suscriptores durante la enumeración.

Hay varias formas de arreglar esto, una cambiando el ciclo for para usar un .ToList():

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

95
2018-03-03 02:13



Una forma más eficiente, en mi opinión, es tener otra lista en la que declare que coloca todo lo que "se va a eliminar". Luego, después de terminar el bucle principal (sin .ToList ()), realiza otro bucle sobre la lista "para eliminar", eliminando cada entrada a medida que sucede. Entonces en tu clase agregas:

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

Luego lo cambias a:

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

Esto no solo resolverá su problema, sino que le evitará tener que seguir creando una lista de su diccionario, que es cara si hay muchos suscriptores allí. Suponiendo que la lista de suscriptores a eliminar en cualquier iteración dada es menor que el número total en la lista, esto debería ser más rápido. Pero, por supuesto, siéntase libre de darle un perfil para asegurarse de que ese es el caso si hay alguna duda en su situación de uso específico.


53
2018-03-03 06:58



También puede bloquear el diccionario de suscriptores para evitar que se modifique siempre que se realice un bucle:

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

33
2018-05-23 19:32



Nota: En general, las colecciones .Net no admiten ser enumeradas y modificadas al mismo tiempo. Si intenta modificar la lista de colecciones mientras se encuentra en el medio de enumerarla, se generará una excepción.

Entonces, el problema detrás de este error es que no podemos modificar la lista / diccionario mientras estamos revisando. Pero si iteramos un diccionario usando una lista temporal de sus claves, en paralelo podemos modificar el objeto del diccionario, porque ahora no estamos iterando el diccionario (e iterando su colección de claves).

muestra: 

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

Aquí hay un entrada en el blogsobre esta solución.

Y para una inmersión profunda en stackoverflow: ¿Por qué ocurre este error?


9
2017-11-11 12:12



En realidad, el problema me parece que está eliminando elementos de la lista y esperando continuar leyendo la lista como si nada hubiera sucedido.

Lo que realmente necesita hacer es comenzar desde el final y volver al principio. Incluso si elimina elementos de la lista, podrá continuar leyéndolos.


4
2018-05-23 16:10



InvalidOperationException-  Se ha producido una excepción InvalidOperationException. Informa que "se modificó una colección" en un bucle foreach

Utilice la declaración de interrupción, una vez que se elimine el objeto.

ex:

ArrayList list = new ArrayList(); 

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

3
2018-03-16 07:20



Tuve el mismo problema, y ​​se solucionó cuando usé un for bucle en lugar 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



He visto muchas opciones para esto, pero para mí esta fue la mejor.

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

Luego simplemente recorre la colección.

Tenga en cuenta que un ListItemCollection puede contener duplicados. Por defecto, no hay nada que impida que se agreguen duplicados a la colección. Para evitar duplicados, puede hacer esto:

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



Bien, entonces lo que me ayudó fue iterar hacia atrás. Estaba tratando de eliminar una entrada de una lista pero iterando hacia arriba y arruinó el ciclo porque la entrada ya no existía:

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

                            myList.RemoveAt(x);

                        }

1
2017-07-22 14:14



Puede copiar el objeto de diccionario de suscriptores en un mismo tipo de objeto de diccionario temporal y luego iterar el objeto de diccionario temporal utilizando el bucle foreach.


0
2018-05-29 13:59