Коллекция была изменена; операция перечисления может не выполняться

Я не могу добраться до сути этой ошибки, потому что когда отладчик присоединен, это, кажется, не происходит. Ниже приведен код.

это сервер WCF в службе Windows. Метод NotifySubscribers вызывается службой всякий раз, когда происходит событие данных (через случайные интервалы, но не очень часто - около 800 раз в день).

когда клиент Windows Forms подписывается, идентификатор подписчика добавляется в словарь подписчиков и когда клиент отписывается, удаляется из словаря. Ошибка возникает, когда (или после) клиент отписывается. Похоже, что при следующем вызове метода NotifySubscribers() цикл foreach () завершается ошибкой в строке темы. Метод записывает ошибку в журнал приложений, как показано в приведенном ниже коде. Когда отладчик подключен и клиент отписывается, код выполняется нормально.

вы видите проблему с этим кодом? Мне нужно сделать словарь потокобезопасный?

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

12 ответов


вероятно, что SignalData косвенно изменяет словарь подписчиков под капотом во время цикла и приводит к этому сообщению. Вы можете проверить это, изменив

foreach(Subscriber s in subscribers.Values)

до

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

Если я прав, проблема исчезнет


когда подписчик отменяет подписку, вы изменяете содержимое коллекции подписчиков во время перечисления.

есть несколько способов исправить это, один из которых изменяет цикл for для использования явного .ToList():

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

более эффективный способ, на мой взгляд, - это иметь другой список, в который вы объявляете, что вы помещаете все, что "должно быть удалено". Затем после того, как вы закончите свой основной цикл (без .ToList ()), вы делаете еще один цикл над списком "быть удаленным", удаляя каждую запись, как это происходит. Поэтому в своем классе вы добавляете:

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

затем вы измените его на:

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

это не только решит вашу проблему, это помешает вам продолжать создавать список из вашего словарь, который стоит дорого, если там много подписчиков. Предполагая, что список абонентов, которые будут удалены на любой итерации, меньше общего числа в списке, это должно быть быстрее. Но, конечно, не стесняйтесь профилировать его, чтобы быть уверенным, что это так, если есть какие-либо сомнения в вашей конкретной ситуации использования.


вы также можете заблокировать словарь подписчиков, чтобы предотвратить его изменение всякий раз, когда его цикл:

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

Примечание: в целом коллекции .Net не поддерживают одновременное перечисление и изменение. Если вы попытаетесь изменить список коллекций во время его перечисления, это вызовет исключение.

таким образом, проблема этой ошибки заключается в том, что мы не можем изменить список/Словарь во время цикла. Но если мы повторим словарь, используя временный список его ключей, параллельно мы можем изменить объект словаря, потому что теперь мы не итерация словаря (и итерация его коллекции ключей).

пример:

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

здесь блоге об этом решении.

и для глубокого погружения в stackoverflow:почему возникает эта ошибка?


на самом деле проблема кажется мне, что вы удаляете элементы из списка и ожидаете, что продолжите читать список, как будто ничего не произошло.

то, что вам действительно нужно сделать, это начать с конца и вернуться к началу. Даже если вы удалите элементы из списка, вы сможете продолжить его чтение.


InvalidOperationException - Произошло исключение InvalidOperationException. Он сообщает, что "коллекция была изменена" в цикле foreach

используйте инструкцию break, как только объект будет удален.

ex:

ArrayList list = new ArrayList(); 

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

у меня была такая же проблема, и она была решена, когда я использовал for петли вместо 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);
}

Я видел много вариантов для этого, но для меня эта была лучшей.

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

потом просто цикл по коллекции.

имейте в виду, что ListItemCollection может содержать дубликаты. По умолчанию ничто не препятствует добавлению дубликатов в коллекцию. Чтобы избежать дублирования, вы можете сделать следующее:

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

хорошо, так что мне помогло повторение назад. Я пытался удалить запись из списка, но итерация вверх, и она испортила цикл, потому что запись больше не существовала:

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

                            myList.RemoveAt(x);

                        }

можно скопировать объект словаря подписчиков в объект временного словаря того же типа, а затем выполнить итерацию объекта временного словаря с помощью цикла foreach.


таким образом, другим способом решения этой проблемы было бы вместо удаления элементов создать новый словарь и добавить только элементы, которые вы не хотели удалять, а затем заменить исходный словарь новым. Я не думаю, что это слишком большая проблема эффективности, потому что это не увеличивает количество раз, когда вы повторяете структуру.