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 subscribers; public SubscriptionServer() { subscribers = new Dictionary(); } 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(); 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); } } } 

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á

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

Hay varias maneras de solucionar esto, una cambiando el ciclo for para usar un .ToList() explícito:

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

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 toBeRemoved = new List(); 

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.

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

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 keys = new List(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 una publicación de blog sobre esta solución.

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

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.

InvalidOperationException : se ha producido una excepción InvalidOperationException. Informa que “se modificó una colección” en un bucle foreach

Utilice la statement 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; } } 

Tuve el mismo problema, y ​​se resolvió cuando utilicé un bucle for 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); } 

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

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

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.

Entonces, una forma diferente de resolver este problema sería en lugar de eliminar los elementos, crear un nuevo diccionario y solo agregar los elementos que no quería eliminar, luego reemplazar el diccionario original por el nuevo. No creo que esto sea demasiado problema de eficiencia porque no aumenta la cantidad de iteraciones sobre la estructura.