C# 集合被修改;枚举操作可能不会执行
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/604831/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Collection was modified; enumeration operation may not execute
提问by cdonner
I can't get to the bottom of this error, because when the debugger is attached, it does not seem to occur. Below is the code.
我无法深究这个错误,因为当附加调试器时,它似乎没有发生。下面是代码。
This is a WCF server in a Windows service. The method NotifySubscribers is called by the service whenever there is a data event (at random intervals, but not very often - about 800 times per day).
这是 Windows 服务中的 WCF 服务器。只要有数据事件(随机间隔,但不是很频繁 - 每天大约 800 次),服务就会调用 NotifySubscribers 方法。
When a Windows Forms client subscribes, the subscriber ID is added to the subscribers dictionary, and when the client unsubscribes, it is deleted from the dictionary. The error happens when (or after) a client unsubscribes. It appears that the next time the NotifySubscribers() method is called, the foreach() loop fails with the error in the subject line. The method writes the error into the application log as shown in the code below. When a debugger is attached and a client unsubscribes, the code executes fine.
当 Windows 窗体客户端订阅时,订阅者 ID 被添加到订阅者字典中,当客户端取消订阅时,它会从字典中删除。当(或之后)客户端取消订阅时会发生错误。似乎下次调用 NotifySubscribers() 方法时,foreach() 循环失败并在主题行中出现错误。该方法将错误写入应用程序日志,如下面的代码所示。当附加调试器并且客户端取消订阅时,代码执行良好。
Do you see a problem with this code? Do I need to make the dictionary thread-safe?
你看到这段代码有问题吗?我需要使字典线程安全吗?
[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);
}
}
}
采纳答案by JaredPar
What's likely happening is that SignalData is indirectly changing the subscribers dictionary under the hood during the loop and leading to that message. You can verify this by changing
可能发生的情况是 SignalData 在循环期间间接更改了引擎盖下的订户字典并导致该消息。您可以通过更改来验证这一点
foreach(Subscriber s in subscribers.Values)
To
到
foreach(Subscriber s in subscribers.Values.ToList())
If I'm right, the problem will dissapear
如果我是对的,问题就会消失
Calling subscribers.Values.ToList()
copies the values of subscribers.Values
to a separate list at the start of the foreach
. Nothing else has access to this list (it doesn't even have a variable name!), so nothing can modify it inside the loop.
调用subscribers.Values.ToList()
将 的值复制subscribers.Values
到foreach
. 没有其他人可以访问这个列表(它甚至没有变量名!),所以没有什么可以在循环内修改它。
回答by Mitch Wheat
When a subscriber unsubscribes you are changing contents of the collection of Subscribers during enumeration.
当订阅者取消订阅时,您正在枚举期间更改订阅者集合的内容。
There are several ways to fix this, one being changing the for loop to use an explicit .ToList()
:
有几种方法可以解决这个问题,一种是将 for 循环更改为使用显式.ToList()
:
public void NotifySubscribers(DataRecord sr)
{
foreach(Subscriber s in subscribers.Values.ToList())
{
^^^^^^^^^
...
回答by x4000
A more efficient way, in my opinion, is to have another list that you declare that you put anything that is "to be removed" into. Then after you finish your main loop (without the .ToList()), you do another loop over the "to be removed" list, removing each entry as it happens. So in your class you add:
在我看来,一种更有效的方法是创建另一个列表,声明将“要删除”的任何内容放入其中。然后在您完成主循环(没有 .ToList())之后,您对“要删除”列表执行另一个循环,在每个条目发生时删除它。所以在你的课上你添加:
private List<Guid> toBeRemoved = new List<Guid>();
Then you change it to:
然后你把它改成:
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 );
}
This will not only solve your problem, it will prevent you from having to keep creating a list from your dictionary, which is expensive if there are a lot of subscribers in there. Assuming the list of subscribers to be removed on any given iteration is lower than the total number in the list, this should be faster. But of course feel free to profile it to be sure that's the case if there's any doubt in your specific usage situation.
这不仅可以解决您的问题,还可以防止您继续从字典中创建列表,如果其中有很多订阅者,这会很昂贵。假设在任何给定迭代中要删除的订阅者列表低于列表中的总数,这应该更快。但当然,如果对您的特定使用情况有任何疑问,请随时对其进行概要分析以确保情况确实如此。
回答by luc.rg.roy
Actually the problem seems to me that you are removing elements from the list and expecting to continue to read the list as if nothing had happened.
实际上,在我看来,问题是您正在从列表中删除元素,并希望继续阅读列表,就好像什么也没发生一样。
What you really need to do is to start from the end and back to the begining. Even if you remove elements from the list you will be able to continue reading it.
你真正需要做的是从结尾开始,然后回到开头。即使您从列表中删除元素,您也可以继续阅读它。
回答by Mohammad Sepahvand
You can also lock your subscribers dictionary to prevent it from being modified whenever its being looped:
您还可以锁定您的订阅者字典以防止它在循环时被修改:
lock (subscribers)
{
foreach (var subscriber in subscribers)
{
//do something
}
}
回答by Rezoan
You can copy subscribers dictionary object to a same type of temporary dictionary object and then iterate the temporary dictionary object using foreach loop.
您可以将订阅者字典对象复制到相同类型的临时字典对象,然后使用 foreach 循环迭代临时字典对象。
回答by ford prefect
So a different way to solve this problem would be instead of removing the elements create a new dictionary and only add the elements you didnt want to remove then replace the original dictionary with the new one. I don't think this is too much of an efficiency problem because it does not increase the number of times you iterate over the structure.
因此,解决此问题的另一种方法是,不是删除元素,而是创建一个新字典,只添加您不想删除的元素,然后用新字典替换原始字典。我不认为这是一个太大的效率问题,因为它不会增加您对结构进行迭代的次数。
回答by Daniel Moreshet
I had the same issue, and it was solved when I used a for
loop instead of foreach
.
我遇到了同样的问题,当我使用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);
}
回答by open and free
Why this error?
为什么会出现这个错误?
In general .Net collections do not support being enumerated and modified at the same time. If you try to modify the collection list during enumeration, it raises an exception. So the issue behind this error is, we can not modify the list/dictionary while we are looping through the same.
一般来说,.Net 集合不支持同时枚举和修改。如果您尝试在枚举期间修改集合列表,则会引发异常。所以这个错误背后的问题是,我们不能在循环时修改列表/字典。
One of the solutions
解决方案之一
If we iterate a dictionary using a list of its keys, in parallel we can modify the dictionary object, as we are iterating through the key-collection and not the dictionary(and iterating its key collection).
如果我们使用它的键列表迭代字典,我们可以同时修改字典对象,因为我们迭代的是键集合而不是字典(并迭代它的键集合)。
Example
例子
//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;
}
Here is a blog postabout this solution.
这是有关此解决方案的博客文章。
And for a deep dive in StackOverflow: Why this error occurs?
深入了解 StackOverflow:为什么会发生此错误?
回答by Mike
I've seen many options for this but to me this one was the best.
我已经看到了很多选择,但对我来说这是最好的。
ListItemCollection collection = new ListItemCollection();
foreach (ListItem item in ListBox1.Items)
{
if (item.Selected)
collection.Add(item);
}
Then simply loop through the collection.
然后简单地循环遍历集合。
Be aware that a ListItemCollection can contain duplicates. By default there is nothing preventing duplicates being added to the collection. To avoid duplicates you can do this:
请注意 ListItemCollection 可以包含重复项。默认情况下,不会阻止将重复项添加到集合中。为避免重复,您可以这样做:
ListItemCollection collection = new ListItemCollection();
foreach (ListItem item in ListBox1.Items)
{
if (item.Selected && !collection.Contains(item))
collection.Add(item);
}