提问



我无法理解这个错误的底部,因为当附加调试器时,它似乎不会发生。下面是代码。


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


当Windows窗体客户端订阅时,订户ID将添加到订阅者字典中,当客户端取消订阅时,将从字典中删除它。客户端取消订阅时(或之后)发生错误。看来,下次调用NotifySubscribers()方法时,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);
        }
    }
}

最佳参考


可能发生的是SignalData在循环期间间接地改变了引擎盖下的订阅者字典并导致该消息。您可以通过更改来验证这一点


foreach(Subscriber s in subscribers.Values)





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


如果我说得对,问题就会消失

其它参考1


当订阅者取消订阅时,您在枚举期间正在更改订阅者集合的内容。


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


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

其它参考2


在我看来,一种更有效的方法是使用另一个列表,声明您将要删除的内容放入其中。然后在完成主循环(没有.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 );
}


这不仅可以解决您的问题,还可以防止您不得不继续从字典中创建列表,如果有很多订阅者,这将是昂贵的。假设在任何给定迭代中要删除的订户列表低于列表中的总数,这应该更快。但是当然可以随意对其进行分析,以确保在您的具体使用情况下有任何疑问的情况。

其它参考3


您还可以锁定订阅者字典,以防止它在循环时被修改:


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

其它参考4


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


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


样品:


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


这是关于此解决方案的博客文章。[17]


而对于stackoverflow的深入探讨:为什么会出现这种错误?

其它参考5


实际上,在我看来,问题是您要从列表中删除元素并期望继续读取列表,就好像什么也没发生过一样。


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

其它参考6


出现InvalidOperationException -
发生InvalidOperationException。它在foreach循环中报告集合被修改


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


例如:


ArrayList list = new ArrayList(); 

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

其它参考7


我遇到了同样的问题,当我使用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);
}

其它参考8


我已经看到了很多这方面的选择,但对我来说这个是最好的选择。


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

其它参考9


好吧所以帮助我的是倒退。我试图从列表中删除一个条目,但向上迭代,它搞砸了循环因为条目不再存在:


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

                            myList.RemoveAt(x);

                        }

其它参考10


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

其它参考11


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