关于C#:集合已修改;枚举操作可能无法执行

Collection was modified; enumeration operation may not execute

我无法弄清这个错误的根本原因,因为当附加了调试器时,它似乎不会发生。下面是代码。

这是Windows服务中的WCF服务器。每当有数据事件时,服务都会调用方法notifysubscribers(以随机间隔,但不是经常调用——大约每天800次)。

当Windows窗体客户端订阅时,订阅服务器ID将添加到订阅服务器字典,当客户端取消订阅时,它将从字典中删除。当客户端取消订阅时(或之后)会发生错误。似乎下次调用notifysubscribers()方法时,foreach()循环将失败,主题行中出现错误。方法将错误写入应用程序日志,如下代码所示。当附加了调试器并且客户端取消订阅时,代码执行良好。

你看到这个代码有问题吗?我需要使字典线程安全吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
[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);
        }
    }
}

可能发生的情况是,在循环过程中,signaldata会间接地更改引擎盖下的订阅者字典,并导致该消息。您可以通过更改

1
foreach(Subscriber s in subscribers.Values)

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

如果我是对的,这个问题将无法解决。

调用subscribers.values.to list()会将subscribers.values的值复制到foreach开头的单独列表中。没有其他人可以访问此列表(它甚至没有变量名!),所以在循环中没有任何内容可以修改它。


当订阅服务器取消订阅时,您将在枚举期间更改订阅服务器集合的内容。

有几种方法可以解决这个问题,其中一种方法是将for循环更改为使用显式.ToList()

1
2
3
4
5
6
public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...


在我看来,一个更有效的方法是再列一个清单,你声明你把任何"要删除"的东西放进去。然后,在完成主循环(不带.to list())之后,在"待删除"列表上执行另一个循环,并根据实际情况删除每个条目。所以在你的课堂上,你会增加:

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

然后将其更改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 );
}

这不仅可以解决您的问题,还可以防止您不得不不断地从字典中创建一个列表,如果其中有大量订户,这是很昂贵的。假设在任何给定迭代中要删除的订阅服务器列表低于列表中的总数,这应该更快。当然,如果您的具体使用情况有任何疑问,请随时对其进行分析,以确保确实如此。


您还可以锁定订阅服务器字典,以防止在循环时对其进行修改:

1
2
3
4
5
6
7
 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }


为什么会出现这样的错误?

通常.NET集合不支持同时枚举和修改。如果在枚举期间尝试修改集合列表,则会引发异常。所以这个错误背后的问题是,当我们循环使用列表/字典时,我们不能修改它。

其中一个解决方案

如果我们使用字典的键列表来迭代字典,我们可以并行地修改字典对象,就像我们迭代键集合和不是字典(并迭代其键集合)。

例子

1
2
3
4
5
6
7
8
9
//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的深入研究:为什么会发生这个错误?


实际上,在我看来,问题似乎是您正在从列表中删除元素,并期望继续像什么都没有发生一样读取列表。

你真正需要做的是从一开始到另一开始。即使从列表中删除元素,也可以继续读取。


无效操作异常-发生InvalidOperationException。它报告foreach循环中的"集合已修改"

删除对象后,使用break语句。

前任:

1
2
3
4
5
6
7
8
9
10
ArrayList list = new ArrayList();

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

我看过很多选择,但对我来说,这是最好的选择。

1
2
3
4
5
6
ListItemCollection collection = new ListItemCollection();
        foreach (ListItem item in ListBox1.Items)
        {
            if (item.Selected)
                collection.Add(item);
        }

然后简单地循环整个集合。

请注意,ListItemCollection可以包含重复项。默认情况下,不会阻止向集合中添加重复项。要避免重复,可以执行以下操作:

1
2
3
4
5
6
ListItemCollection collection = new ListItemCollection();
            foreach (ListItem item in ListBox1.Items)
            {
                if (item.Selected && !collection.Contains(item))
                    collection.Add(item);
            }


我也有同样的问题,当我使用一个for循环而不是foreach循环时,问题就解决了。

1
2
3
4
5
6
7
8
9
10
11
12
// 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
2
3
4
5
6
for (int x = myList.Count - 1; x > -1; x--)
                        {

                            myList.RemoveAt(x);

                        }

这里有一个链接,它非常详细地阐述了解决方案。如果你有合适的解决方案,请尝试一下,请在这里张贴,以便其他人可以理解。给定的解决方案是可以的,然后像帖子那样,其他人可以尝试这些解决方案。

供您参考原始链接:https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-products-collection-was-modified-enumeration-operation-may-not-execute/

当我们使用.NET序列化类对其定义包含可枚举类型的对象进行序列化时,即。集合,您将很容易得到InvalidOperationException,即"集合已修改;如果您的编码在多线程方案下,枚举操作可能不会执行。根本原因是序列化类将通过枚举器迭代集合,例如,问题出在修改集合时尝试迭代它。

第一个解决方案,我们可以简单地使用锁作为同步解决方案,以确保对列表对象的操作一次只能从一个线程执行。很明显,你会得到性能惩罚如果要序列化该对象的集合,则将为每个对象应用锁。

很好,.NET 4.0,这使得处理多线程场景变得方便。对于这个序列化收集字段问题,我发现我们可以从ConcurrentQueue(check msdn)类中获益。这是一个线程安全的FIFO集合,使代码锁自由。

使用这个类,简单地说,您需要为代码修改的内容是用它替换集合类型,使用enqueue将元素添加到concurrentqueue的末尾,删除这些锁代码。或者,如果您正在处理的场景确实需要像list这样的集合内容,那么您还需要一些代码来将ConcurrentQueue适应到您的字段中。

顺便说一句,由于底层算法不允许自动清除集合,所以ConcurrentQueue没有清晰的方法。所以你必须自己做,最快的方法是重新创建一个空的ConcurrentQueue来替换它。


因此,解决这个问题的另一种方法是,不要删除元素,而是创建一个新字典,只添加不想删除的元素,然后用新字典替换原来的字典。我不认为这是一个效率问题,因为它不会增加您在结构上迭代的次数。


可以将订阅服务器字典对象复制到同一类型的临时字典对象,然后使用foreach循环迭代临时字典对象。