A coleção foi modificada; operação de enumeração pode não ser executada

911

Não consigo chegar ao fundo desse erro, porque quando o depurador é anexado, ele não parece 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 aleatórios, mas não com muita frequência - cerca de 800 vezes por dia).

Quando um cliente Windows Forms se inscreve, o ID do assinante é adicionado ao dicionário de assinantes e, quando o cliente cancela a inscrição, é excluído do dicionário. O erro ocorre quando (ou depois) um cliente cancela a inscrição. Parece que na próxima vez que o método NotifySubscribers () for chamado, o loop foreach () falhará 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 dicionário seguro para 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
fonte
no meu caso, foi um efeito colateral porque eu estava usando alguns .Include ("table") que foram modificados durante o processo - não muito óbvio ao ler o código. no entanto, tive a sorte de não incluir essas inclusões (sim! código antigo e sem manutenção) e resolvi meu problema removendo-as.
Adi

Respostas:

1631

O que provavelmente está acontecendo é que o SignalData está indiretamente alterando o dicionário de assinantes sob o capô durante o loop e levando a essa mensagem. Você pode verificar isso alterando

foreach(Subscriber s in subscribers.Values)

Para

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

Se eu estiver certo, o problema desaparecerá

A chamada subscribers.Values.ToList()copia os valores de subscribers.Valuespara uma lista separada no início do foreach. Nada mais tem acesso a essa lista (ele nem sequer tem um nome de variável!); Portanto, nada pode modificá-lo dentro do loop.

JaredPar
fonte
14
BTW .ToList () está presente na DLL System.Core que não é compatível com os aplicativos .NET 2.0. Então você pode precisar alterar seu aplicativo alvo a Net 3.5
mishal153
60
Eu não entendo por que você fez um ToList e por que correções de tudo
PositiveGuy
204
@ CoffeeAddict: O problema é que subscribers.Valuesestá sendo modificado dentro do foreachloop. A chamada subscribers.Values.ToList()copia os valores de subscribers.Valuespara uma lista separada no início do foreach. Nada mais tem acesso a esta lista (nem sequer tem um nome de variável!) , Portanto, nada pode modificá-la dentro do loop.
precisa saber é o seguinte
31
Observe que ToListtambém pode ser lançado se a coleção for modificada enquanto ToListestiver em execução.
Sriram Sakthivel
16
Tenho certeza de que acho que não resolve o problema, mas apenas dificulta a reprodução. ToListnão é uma operação atômica. O que é ainda mais engraçado: ToListbasicamente faz o seu próprio foreachinternamente para copiar itens para uma nova instância da lista, o que significa que você corrigiu um foreachproblema adicionando uma foreachiteração adicional (embora mais rápida) .
Groo
113

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 uma delas alterar o loop for para usar um explícito .ToList():

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

Uma maneira mais eficiente, na minha opinião, é ter outra lista na qual você declara colocar tudo o que "deve ser removido". Depois que você terminar o loop principal (sem o .ToList ()), faça outro loop na lista "a ser removido", removendo cada entrada conforme ela ocorrer. Então na sua turma você adiciona:

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

Então você o altera 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á o seu problema, como também impedirá que você continue criando uma lista do seu dicionário, o que é caro se houver muitos assinantes. Supondo que a lista de assinantes a ser removida em qualquer iteração fornecida 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 garantir que esse seja o caso, se houver alguma dúvida em sua situação específica de uso.

x4000
fonte
9
Espero que valha a pena considerar se você tiver um e estiver trabalhando com coleções maiores. Se pequeno, provavelmente eu apenas ToList e seguir em frente.
Karl Kieninger
42

Você também pode bloquear o dicionário de inscritos para impedir que seja modificado sempre que houver um loop:

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }
Mohammad Sepahvand
fonte
2
Esse é um exemplo completo? Eu tenho uma classe (_dictionary obj abaixo) que contém um Dictionary genérico <string, int> chamado MarkerFrequencies, mas isso não resolveu instantaneamente a falha: lock (_dictionary.MarkerFrequencies) {foreach (KeyValuePair <string, int> pair in _dictionary.MarkerFrequencies) {...}}
Jon Coombs
4
@JCoombs, é possível que você esteja modificando, possivelmente reatribuindo o MarkerFrequenciesdicionário dentro do próprio bloqueio, o que significa que a instância original não está mais bloqueada. Tente também usar a em forvez de a foreach, veja isto e isto . Deixe-me saber se isso resolve.
Mohammad Sepahvand
1
Bem, finalmente consegui corrigir esse bug, e essas dicas foram úteis - obrigado! Meu conjunto de dados era pequeno e essa operação era apenas uma atualização de exibição da interface do usuário; portanto, iterar sobre uma cópia parecia melhor. (Também experimentei usar for, em vez de foreach, depois de bloquear MarkerFrequencies nos dois threads. Isso evitou a falha, mas parecia exigir mais trabalho de depuração. E introduziu mais complexidade. Minha única razão para ter dois threads aqui é permitir que o usuário cancele a propósito, a interface do usuário. A interface do usuário não modifica esses dados diretamente.)
Jon Coombs
9
O problema é que, para aplicativos grandes, os bloqueios podem ser um grande problema de desempenho - é melhor usar uma coleção no System.Collections.Concurrentespaço para nome.
BrainSlugs83
28

Por que esse erro?

Em geral, as coleções .Net não oferecem suporte para serem enumeradas e modificadas ao mesmo tempo. Se você tentar modificar a lista de coleções durante a enumeração, isso gera uma exceção. Portanto, o problema por trás desse erro é que não podemos modificar a lista / dicionário enquanto percorremos o mesmo.

Uma das soluções

Se iterarmos um dicionário usando uma lista de suas chaves, em paralelo, podemos modificar o objeto de dicionário, pois estamos repetindo a coleção de chaves e não o dicionário (e repetindo sua coleção de chaves).

Exemplo

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

Aqui está um post sobre esta solução.

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

aberto e livre
fonte
Obrigado! Uma explicação clara de por que isso acontece e imediatamente me deu o caminho para resolver isso no meu aplicativo.
Kim Crosser
5

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 fim e voltar ao início. Mesmo se você remover elementos da lista, poderá continuar a lê-lo.

luc.rg.roy
fonte
Não vejo como isso faria alguma diferença? Se você remover um elemento no meio da lista, ele ainda gera um erro, pois o item que está tentando acessar não pode ser encontrado?
Zapnologica
4
@ Zapnologica a diferença é - você não estaria enumerando a lista - em vez de fazer um for / each, você faria um for / next e acessá-lo por número inteiro - você pode definitivamente modificar uma lista em um loop for / next, mas nunca em um loop for / each (porque for / each enumera ) - você também pode avançar em um for / next, desde que tenha lógica extra para ajustar seus contadores, etc.
BrainSlugs83
4

InvalidOperationException - Ocorreu uma InvalidOperationException. Ele informa que uma "coleção foi modificada" em um loop foreach

Use a instrução break, uma vez que o objeto é removido.

ex:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}
vivek
fonte
Solução boa e simples, se você souber que sua lista tem no máximo um item que precisa ser removido.
Tawab Wakil
3

Eu tive o mesmo problema e foi resolvido quando usei um forloop 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);
}
Daniel Moreshet
fonte
11
Este código está errado e pulará alguns itens da coleção se algum elemento for removido. Por exemplo: você tem var arr = ["a", "b", "c"] e na primeira iteração (i = 0) você remove o elemento na posição 0 (elemento "a"). Depois disso, todos os elementos da matriz se moverão uma posição para cima e a matriz será ["b", "c"]. Portanto, na próxima iteração (i = 1), você verificará o elemento na posição 1, que será "c" e não "b". Isto está errado. Para corrigir isso, você tem que se deslocar de baixo para cima
Kaspars Ozols
3

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

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

                            myList.RemoveAt(x);

                        }
Mark Aven
fonte
2

Eu já vi muitas opções para isso, mas para mim essa foi a 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, não há nada que impeça a adição de duplicados à 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);
            }
Mike
fonte
Como esse código evitaria que duplicatas fossem inseridas no banco de dados. Escrevi algo semelhante e, quando adiciono novos usuários da caixa de listagem, se mantiver acidentalmente um selecionado que já esteja na lista, ele criará uma entrada duplicada. Você tem alguma sugestão @ Mike?
Jamie
1

A resposta aceita é imprecisa e incorreta no pior dos casos. Se forem feitas alterações duranteToList() , você ainda poderá acabar com um erro. Além disso lock, qual desempenho e segurança de encadeamento precisam ser levados em consideração se você tiver um membro público, uma solução adequada pode ser usar tipos imutáveis .

Em geral, um tipo imutável significa que você não pode alterar o estado dele depois de criado. Portanto, seu código deve se parecer com:

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

Isso pode ser especialmente útil se você tiver um membro público. Outras classes podem sempre foreachnos tipos imutáveis ​​sem se preocupar com a coleção que está sendo modificada.

Joe
fonte
1

Aqui está um cenário específico que merece uma abordagem especializada:

  1. O Dictionaryé enumerado com freqüência.
  2. O Dictionaryé modificado com pouca frequência.

Nesse cenário, criar uma cópia do Dictionary(ou do Dictionary.Values) antes de toda enumeração pode ser bastante dispendiosa. Minha idéia sobre como resolver esse problema é reutilizar a mesma cópia em cache em várias enumerações e assistir a um IEnumeratororiginal em Dictionarybusca de exceções. O enumerador será armazenado em cache junto com os dados copiados e interrogado antes de iniciar uma nova enumeração. No caso de uma exceção, a cópia em cache será descartada e uma nova será criada. Aqui está a minha implementação desta ideia:

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

Exemplo de uso:

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)
{
    //...
}

Infelizmente, esta ideia não pode ser utilizado atualmente com a classe Dictionaryno .NET núcleo 3.0, porque esta classe não lançar uma coleção foi modificada exceção quando enumerado e os métodos Removee Clearsão invocados. Todos os outros contêineres que verifiquei estão se comportando de maneira consistente. Eu verifiquei sistematicamente essas classes: List<T>, Collection<T>, ObservableCollection<T>, HashSet<T>, SortedSet<T>, Dictionary<T,V>e SortedDictionary<T,V>. Somente os dois métodos mencionados da Dictionaryclasse no .NET Core não estão invalidando a enumeração.


Atualização: Corrigi o problema acima comparando também os comprimentos da coleção em cache e da original. Esta solução assume que o dicionário vai ser passada directamente como um argumento para o EnumerableSnapshotconstrutor 's, e a sua identidade não será escondido por (por exemplo) uma projecção como: dictionary.Select(e => e).ΤοEnumerableSnapshot().


Importante: A classe acima não é segura para threads. Destina-se a ser usado a partir de código executado exclusivamente em um único encadeamento.

Theodor Zoulias
fonte
0

Você pode copiar o objeto de dicionário de assinantes para um mesmo tipo de objeto de dicionário temporário e, em seguida, iterar o objeto de dicionário temporário usando o loop foreach.

Rezoan
fonte
(Esta postagem parece não fornecer uma resposta de qualidade à pergunta. Edite sua resposta e, ou apenas publique como um comentário na pergunta).
sɐunıɔ ןɐ qɐp
0

Portanto, uma maneira diferente de resolver esse problema seria, em vez de remover os elementos, criar um novo dicionário e adicionar apenas os elementos que você não deseja remover e substituir o dicionário original pelo novo. Eu não acho que isso seja um problema de eficiência porque não aumenta o número de vezes que você itera sobre a estrutura.

ford prefeito
fonte
0

Há um link onde ele elaborou muito bem e a solução também é fornecida. Experimente se você tiver uma solução adequada, poste aqui para que outros possam entender. Dada solução está ok, então, como o post para que outros possam tentar essas soluções.

para você referenciar o link original: - https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

Quando usamos classes .Net Serialization para serializar um objeto em que sua definição contém um tipo Enumerable, ou seja, coleção, você receberá facilmente InvalidOperationException dizendo "Coleção foi modificada; operação de enumeração pode não ser executada" onde sua codificação está em cenários de vários segmentos. A causa inferior é que as classes de serialização irão percorrer a coleção via enumerador; assim, o problema é tentar percorrer uma coleção enquanto a modifica.

Primeira solução, podemos simplesmente usar o bloqueio como uma solução de sincronização para garantir que a operação no objeto List possa ser executada apenas a partir de um encadeamento por vez. Obviamente, você receberá uma penalidade de desempenho que, se desejar serializar uma coleção desse objeto, para cada um deles, o bloqueio será aplicado.

Bem, o .Net 4.0, que facilita o manuseio de cenários com vários threads. para esse problema de campo da coleção de serialização, descobri que podemos tirar proveito da classe ConcurrentQueue (Check MSDN), que é uma coleção FIFO e segura para threads e torna o código livre de bloqueios.

Usando esta classe, em sua simplicidade, as coisas que você precisa modificar para o seu código estão substituindo o tipo de Coleção por ele, use Enfileirar para adicionar um elemento ao final do ConcurrentQueue, remova esses códigos de bloqueio. Ou, se o cenário em que você está trabalhando exigir itens de coleção como List, você precisará de mais um código para adaptar o ConcurrentQueue em seus campos.

BTW, ConcurrentQueue não possui um método Clear devido ao algoritmo subjacente que não permite a limpeza atomizada da coleção. para que você faça isso sozinho, a maneira mais rápida é recriar um novo ConcurrentQueue vazio para substituição.

user8851697
fonte
-1

Pessoalmente, deparei com isso recentemente em código desconhecido, onde estava em um dbcontext aberto com o .Remove (item) do Linq. Tive muita dificuldade em localizar a depuração de erros porque os itens foram removidos na primeira vez em que iteramos e, se eu tentasse dar um passo para trás e refazer a etapa, a coleção estava vazia, pois eu estava no mesmo contexto!

Michael Sefranek
fonte