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原来的字典。 我不认为这是一个效率问题,因为它不会增加迭代结构的次数。