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

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

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

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

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

[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);
        }
    }
}
вопрос задан 3.03.2009
cdonner
24611 репутация

12 ответов


  • 1311 рейтинг

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

    foreach(Subscriber s in subscribers.Values)
    

    до

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

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

    ответ дан JaredPar, с репутацией 555649, 3.03.2009
  • 97 рейтинг

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

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

    public void NotifySubscribers(DataRecord sr)  
    {
        foreach(Subscriber s in subscribers.Values.ToList())
        {
                                                  ^^^^^^^^^  
            ...
    
    ответ дан Mitch Wheat, с репутацией 248826, 3.03.2009
  • 54 рейтинг

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

    private List toBeRemoved = new List();
    

    Затем измените его на:

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

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

    ответ дан x4000, с репутацией 1857, 3.03.2009
  • 33 рейтинг

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

     lock (subscribers)
     {
             foreach (var subscriber in subscribers)
             {
                   //do something
             }
     }
    
    ответ дан Mohammad Sepahvand, с репутацией 13346, 23.05.2012
  • 9 рейтинг

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

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

    образец:

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

    Вот блог об этом решении.

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

    ответ дан open and free, с репутацией 1412, 11.11.2014
  • 4 рейтинг

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

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

    ответ дан luc.rg.roy, с репутацией 41, 23.05.2012
  • 3 рейтинг

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

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

    ex:

    ArrayList list = new ArrayList(); 
    
    foreach (var item in list)
    {
        if(condition)
        {
            list.remove(item);
            break;
        }
    }
    
    ответ дан vivek, с репутацией 31, 16.03.2017
  • 2 рейтинг

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

    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);
                }
    
    ответ дан Mike, с репутацией 133, 4.03.2015
  • 2 рейтинг

    У меня была та же проблема, и она была решена, когда я использовал петлю 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);
    }
    
    ответ дан Daniel Moreshet, с репутацией 61, 16.06.2014
  • 1 рейтинг

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

    for (int x = myList.Count - 1; x > -1; x--)
                            {
    
                                myList.RemoveAt(x);
    
                            }
    
    ответ дан Mark Aven, с репутацией 20, 22.07.2018
  • 0 рейтинг

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

    ответ дан Rezoan, с репутацией 1131, 29.05.2013
  • 0 рейтинг

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

    ответ дан ford prefect, с репутацией 2590, 4.10.2013