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


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

это сервер 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 714

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())
    {
                                              ^^^^^^^^^  
        ...
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.

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