Коллекция была изменена; операция перечисления может не выполняться - PullRequest
806 голосов
/ 03 марта 2009

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

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

Когда клиент Windows Forms подписывается, идентификатор подписчика добавляется в словарь подписчиков, а когда клиент отписывается, он удаляется из словаря. Ошибка происходит, когда (или после) клиент отписывается. Похоже, что при следующем вызове метода NotifySubscribeers () цикл 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);
        }
    }
}

Ответы [ 13 ]

1441 голосов
/ 03 марта 2009

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

foreach(Subscriber s in subscribers.Values)

К

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

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

Вызов подписчиков. Value.ToList () копирует значения подписчиков. Значения в отдельный список в начале каждого foreach. Ничто другое не имеет доступа к этому списку (у него даже нет имени переменной!), Поэтому ничто не может изменить его внутри цикла.

107 голосов
/ 03 марта 2009

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

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

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...
58 голосов
/ 03 марта 2009

Более эффективный способ, на мой взгляд, состоит в том, чтобы иметь другой список, в который вы объявляете, что вы помещаете все, что «должно быть удалено». Затем, после завершения основного цикла (без .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 );
}

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

40 голосов
/ 23 мая 2012

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

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }
19 голосов
/ 11 ноября 2014

Почему эта ошибка?

Обычно коллекции .Net не поддерживают одновременное перечисление и изменение. Если вы попытаетесь изменить список коллекции во время перечисления, это вызовет исключение. Таким образом, проблема, стоящая за этой ошибкой, заключается в том, что мы не можем изменить список / словарь, пока мы повторяем его.

Одно из решений

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

Пример

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

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

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

5 голосов
/ 23 мая 2012

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

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

3 голосов
/ 16 марта 2017

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

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

Например:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}
2 голосов
/ 04 марта 2015

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

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);
            }
2 голосов
/ 16 июня 2014

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

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

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

                            myList.RemoveAt(x);

                        }
...