collections被修改; 枚举操作可能无法执行

我不能深究这个错误,因为当debugging器被连接时,似乎不会发生。 下面是代码。

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

当Windows Forms客户端订阅时,订阅者ID被添加到订阅者字典中,并且当客户端取消订阅时,它从字典中被删除。 错误发生在(或之后)客户端取消订阅。 看起来下一次调用NotifySubscribers()方法时,foreach()循环将失败,并在主题行中发生错误。 该方法将错误写入应用程序日志中,如下面的代码所示。 当一个debugging器被连接并且客户端取消订阅时,代码执行正常。

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

[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在循环过程中间接地改变了用户词典中的用户词典并导致该消息。 您可以通过更改来validation

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

在我看来,更有效率的方法是另外列出一个你声明你将任何“被删除”的东西。 然后,在完成主循环(没有.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 ); } 

这不仅可以解决你的问题,还可以防止你不断地从你的字典中创build一个列表,如果有很多订阅者,这个列表是很贵的。 假设在任何给定迭代中要删除的用户列表都低于列表中的总数,这应该会更快。 但是,当然,如果在您的具体使用情况中有任何疑问,可以自由地对其进行描述以确保是这种情况。

您还可以locking您的订阅者词典,以防止它在循环时被修改:

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

注意 :通常.Net集合不支持同时枚举和修改。 如果在列举枚举时尝试修改集合列表,则会引发exception。

所以这个错误背后的问题是,我们无法修改列表/字典,而我们正在循环。 但是如果我们使用它的键的临时列表迭代一个字典,我们可以并行修改字典对象,因为现在我们不迭代字典(并迭代它的键集合)。

样品:

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

这里是关于这个解决scheme的博客文章

而对于一个深入的stackoverflow潜水: 为什么会出现这个错误?

其实问题在我看来,你是从列表中删除元素,并期望继续阅读清单,就好像什么都没有发生。

你真正需要做的是从头到尾回到开始。 即使从列表中删除元素,您仍然可以继续阅读。

我见过很多select,但对我来说这是最好的。

 ListItemCollection collection = new ListItemCollection(); foreach (ListItem item in ListBox1.Items) { if (item.Selected) collection.Add(item); } 

然后简单地循环收集。

InvalidOperationException –发生InvalidOperationException。 它在foreach循环中报告“已被修改的集合”

使用break语句,一旦对象被删除。

例如:

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

我有同样的问题,当我使用“for”循环,而不是foreach解决了

  for (int i = 0; i < itemsToBeLast.Count; i++) //foreach (var item in itemsToBeLast) { var matchingItem = itemsToBeLast.FirstOrDefault(item => item.Detach); if (matchingItem != null) { itemsToBeLast.Remove(matchingItem); continue; } allItems.Add(itemsToBeLast[i]);// (attachDetachItem); } 

您可以将订阅者字典对象复制到相同types的临时字典对象,然后使用foreach循环迭代临时字典对象。

所以解决这个问题的另一种方法是不去除创build新字典的元素,只添加你不想删除的元素,然后用新的字典replace原来的字典。 我不认为这是一个效率问题,因为它不会增加迭代结构的次数。