Coleção foi modificada; operação de enumeração não pode executar

Eu não posso chegar ao fundo deste erro, porque quando o depurador é anexado, parece não ocorrer. Abaixo está o código.

Este é um servidor WCF em um serviço do Windows. O método NotifySubscribers é chamado pelo serviço sempre que houver um evento de dados (em intervalos randoms, mas não com muita frequência – cerca de 800 vezes por dia).

Quando um cliente do Windows Forms se inscreve, o ID do assinante é adicionado ao dictionary de assinantes e, quando o cliente cancela a inscrição, ele é excluído do dictionary. O erro ocorre quando (ou depois) um cliente cancela a inscrição. Parece que a próxima vez que o método NotifySubscribers () é chamado, o loop foreach () falha com o erro na linha de assunto. O método grava o erro no log do aplicativo, conforme mostrado no código abaixo. Quando um depurador é anexado e um cliente cancela a inscrição, o código é executado corretamente.

Você vê algum problema com este código? Preciso tornar o dictionary seguro para thread?

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

O que provavelmente está acontecendo é que o SignalData está indiretamente alterando o dictionary de assinantes durante o loop e levando a essa mensagem. Você pode verificar isso mudando

 foreach(Subscriber s in subscribers.Values) 

Para

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

Se estou certo, o problema vai desaparecer

Quando um assinante cancela a inscrição, você está alterando o conteúdo da coleção de Assinantes durante a enumeração.

Existem várias maneiras de corrigir isso, sendo que uma delas está alterando o loop for para usar um .ToList() explícito:

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

Uma maneira mais eficiente, na minha opinião, é ter uma outra lista que você declara que você coloca qualquer coisa que “seja removida”. Então, depois de terminar seu loop principal (sem o .ToList ()), você faz outro loop sobre a lista “a ser removida”, removendo cada input conforme ela acontece. Então, na sua aula, você adiciona:

 private List toBeRemoved = new List(); 

Então você muda para:

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

Isso não apenas resolverá seu problema, mas também impedirá que você tenha que continuar criando uma lista no seu dictionary, o que é caro se houver muitos assinantes lá. Supondo que a lista de assinantes a serem removidos em qualquer iteração seja menor que o número total na lista, isso deve ser mais rápido. Mas, claro, sinta-se à vontade para criar um perfil para ter certeza de que é o caso, se houver alguma dúvida em sua situação específica de uso.

Você também pode bloquear seu dictionary de assinantes para impedir que ele seja modificado sempre que estiver sendo colocado em loop:

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

Nota : Em geral, as collections .Net não suportam serem enumeradas e modificadas ao mesmo tempo. Se você tentar modificar a lista de collections enquanto estiver no meio da enumeração, ela gerará uma exceção.

Portanto, o problema por trás desse erro é que não podemos modificar a lista / dictionary enquanto estamos passando. Mas se iteramos um dictionary usando uma lista temporária de suas chaves, em paralelo podemos modificar o object do dictionary, porque agora não estamos iterando o dictionary (e iterando sua coleção de chaves).

amostra:

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

Aqui está uma postagem do blog sobre essa solução.

E para um mergulho profundo no stackoverflow: Por que esse erro ocorre?

Na verdade, o problema me parece que você está removendo elementos da lista e esperando continuar a ler a lista como se nada tivesse acontecido.

O que você realmente precisa fazer é começar do final e voltar ao início. Mesmo se você remover elementos da lista, você poderá continuar lendo.

InvalidOperationException- Ocorreu uma InvalidOperationException. Ele relata uma “coleção foi modificada” em um foreach-loop

Use break statement, uma vez que o object é removido.

ex:

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

Eu tive o mesmo problema, e foi resolvido quando eu usei um loop for em vez 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); } 

Eu vi muitas opções para isso, mas para mim este foi o melhor.

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

Em seguida, basta percorrer a coleção.

Esteja ciente de que um ListItemCollection pode conter duplicatas. Por padrão, nada impede que duplicatas sejam adicionadas à coleção. Para evitar duplicatas, você pode fazer isso:

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

Ok, então o que me ajudou foi repetir para trás. Eu estava tentando remover uma input de uma lista, mas iterando para cima e estragou o loop porque a input não existia mais:

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

Você pode copiar o object de dictionary de assinantes para um mesmo tipo de object de dictionary temporário e depois iterar o object de dictionary temporário usando o loop foreach.

Portanto, uma maneira diferente de resolver esse problema seria, em vez de remover os elementos, criar um novo dictionary e adicionar apenas os elementos que você não deseja remover. Em seguida, substitua o dictionary original pelo novo. Não acho que isso seja um problema de eficiência, porque não aumenta o número de vezes que você faz a iteração na estrutura.