it-roy-ru.com

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

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

Это сервер 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);
        }
    }
}
760
cdonner

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

foreach(Subscriber s in subscribers.Values)

К 

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

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

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

1382
JaredPar

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

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

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

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

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

55
x4000

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

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

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

В целом коллекции .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: Почему возникает эта ошибка?

12
open and free

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

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

4
luc.rg.roy

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

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

например: 

ArrayList list = new ArrayList(); 

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

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

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
Mike

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

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

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

                            myList.RemoveAt(x);

                        }
1
Mark Aven

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

0
Rezoan

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

для вас ссылка на оригинальную ссылку: -  https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

Когда мы используем классы .Net Serialization для сериализации объекта, где его определение содержит тип Enumerable, то есть Collection, вы легко получите InvalidOperationException, говорящее «Коллекция была изменена; операция перечисления Может не выполняться» там, где кодируется в многопоточных сценариях . Основная причина состоит в том, что классы сериализации будут проходить через коллекцию через перечислитель, поэтому проблема заключается в попытке перебрать коллекцию при ее изменении.

Первое решение, мы можем просто использовать блокировку в качестве решения для синхронизации, чтобы гарантировать, что Операция с объектом List может выполняться только из одного потока за раз . Очевидно, вы получите снижение производительности за то, что Если вы хотите сериализовать коллекцию этого объекта, то для каждого из них будет применена блокировка.

Ну, .Net 4.0, который делает работу с многопоточными сценариями удобной. Для этой проблемы с сериализацией поля Collection я обнаружил, что мы можем просто воспользоваться преимуществами класса ConcurrentQueue (Check MSDN), , который является потокобезопасным и коллекцией FIFO и делает код свободным от блокировки.

Используя этот класс, в его простоте материал, который вам нужно изменить для своего кода, заменяет тип Collection на него, Используйте Enqueue, чтобы добавить элемент в конец ConcurrentQueue, удалите код блокировки. Или, если сценарий, над которым вы работаете, требует таких сборщиков, как List, вам потребуется еще немного кода для адаптации ConcurrentQueue к вашим полям.

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

0
Meghraj

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

0
ford prefect