【问题标题】:Why does enumerating through a collection throw an exception but looping through its items does not为什么通过集合枚举会引发异常但遍历其项目不会
【发布时间】:2009-04-11 01:27:17
【问题描述】:

我正在测试一些同步结构,我注意到一些让我感到困惑的东西。当我枚举一个集合同时写入它时,它抛出了一个异常(这是预期的),但是当我使用 for 循环遍历集合时,它没有。有人可以解释一下吗?我认为 List 不允许读取器和写入器同时操作。我本来希望循环遍历集合会表现出与使用枚举器相同的行为。

更新:这是一个纯粹的学术练习。我知道如果同时写入列表,枚举列表是不好的。我也明白我需要一个同步构造。我的问题再次是关于为什么一个操作按预期抛出异常,而另一个却没有。

代码如下:

   class Program
   {
    private static List<string> _collection = new List<string>();
    static void Main(string[] args)
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(AddItems), null);
        System.Threading.Thread.Sleep(5000);
        ThreadPool.QueueUserWorkItem(new WaitCallback(DisplayItems), null);
        Console.ReadLine();
    }

    public static void AddItems(object state_)
    {
        for (int i = 1; i <= 50; i++)
        {
            _collection.Add(i.ToString());
            Console.WriteLine("Adding " + i);
            System.Threading.Thread.Sleep(150);
        }
    }

    public static void DisplayItems(object state_)
    {
        // This will not throw an exception
        //for (int i = 0; i < _collection.Count; i++)
        //{
        //    Console.WriteLine("Reading " + _collection[i]);
        //    System.Threading.Thread.Sleep(150);
        //}

        // This will throw an exception
        List<string>.Enumerator enumerator = _collection.GetEnumerator();
        while (enumerator.MoveNext())
        {
            string value = enumerator.Current;
            System.Threading.Thread.Sleep(150);
            Console.WriteLine("Reading " + value);
        }
    }
}

【问题讨论】:

  • 为什么使用 for 或 while 循环很重要? (不是想成为白痴,只是想知道)
  • 区别不在于for和while。问题是在这两种情况下,我都是在写入集合时读取它,那么为什么行为会有所不同。
  • 请看下面我的回答...
  • 从所有 for/while 循环(包括 Add 循环)中删除 Thread.Sleep。你们看到不同的行为吗?
  • 即使从循环中删除睡眠调用后,行为仍然存在。

标签: c# .net synchronization list enumerators


【解决方案1】:

枚举时不能修改集合。即使没有考虑线程问题,该规则仍然存在。来自MSDN

只要集合保持不变,枚举数就保持有效。如果对集合进行了更改,例如添加、修改或删除元素,则枚举器将不可恢复地失效并且其行为未定义。

基于整数的 for 循环实际上不是枚举器。在大多数情况下是完成同样的事情。但是,IEnumerator 的接口保证您可以遍历整个集合。如果在修改集合后发生对 MoveNext 的调用,则平台通过引发异常在内部强制执行此操作。这个异常是由枚举器对象抛出的。

基于整数的 for 循环仅遍历其数字列表。当您按整数索引集合时,您只是在该位置获取项目。如果从列表中插入或删除了某些内容,您可以跳过一个项目或运行相同的项目两次。当您需要在遍历集合时修改集合时,这在某些情况下很有用。 for 循环没有枚举器对象来保证 IEnumerator 契约,因此不会抛出异常。

【讨论】:

  • 这是正确的,但它实际上并没有解释为什么发布的代码会抛出。
  • 鉴于 Thread.Sleep() 调用的长度,看起来代码是专门为违反 IEnumerator 设计的。我将这个问题理解为“这两种迭代策略有何不同?”
  • @Matt Brunell,我们有你的帖子的持续运行历史,所以你不需要用“更新:”或“编辑:”来增加你的答案。如果我愿意,我可以一键查看您所做的编辑。只要努力使您的帖子尽可能清晰,而不需要伪更新/编辑标签。很好的答案。
【解决方案2】:

回答您的实际问题...

枚举时,您将获得一个 IEnumerator,它与您请求时一样绑定到列表的状态。对枚举器(MoveNext,Current)进行进一步的操作。

当使用 for 循环时,如果调用按索引获取特定项目,您将创建一个序列。没有诸如枚举器之类的外部上下文知道您处于循环中。对于所有收藏都知道,您只需要一件物品。由于集合从未分发过枚举器,因此它无法知道您要求第 0 项、第 1 项、第 2 项等的原因是因为您正在遍历列表。

如果您在遍历列表的同时对列表进行处理,那么无论哪种方式,您都会遇到错误。如果添加项目,那么 for 循环可能会静默跳过一些,而 foreach 循环会抛出。如果删除项目,那么如果你不走运,for 循环可能会抛出一个超出范围的索引,但可能大部分时间都可以工作。

但我想你明白这一切,你的问题只是为什么两种迭代方式的行为不同。答案是当您在一种情况下调用 GetEnumerator 和在另一种情况下调用 get_Item 时,集合的状态是已知的(对集合而言)。

【讨论】:

    【解决方案3】:

    不同之处在于,当您说“循环遍历集合”时,实际上并不是循环遍历集合,而是遍历 1 到 50 之间的整数,并在这些索引处添加到集合中。这对 1 到 50 之间的数字仍然存在这一事实没有影响。

    当您枚举列表时,您是在枚举项目,而不是索引。因此,当您在枚举时添加项目时,会使枚举无效。它的构建方式是为了防止像您正在做的事情这样的情况,您可能会枚举到列表中的第 6 项,同时在索引 6 处插入一个项目,您可能会枚举旧的或新的项目,或者一些未定义的状态。

    如果您想这样做,请寻找“线程安全”列表,但要准备好同时处理读写的不准确性:)

    【讨论】:

      【解决方案4】:

      列表更改后,枚举器将变为无效。如果您在枚举列表时更改列表,则需要重新考虑您的策略。

      开始显示功能时获取一个新的枚举器,并在此过程中锁定列表。或者,将您的 List 复制到一个新的 _displayCollection List 中,并枚举这个单独的集合,除非在显示过程开始之前被填充,否则不会写入该集合。希望这会有所帮助。

      【讨论】:

      • 这并不能完全解释为什么贴出的代码 sn-p 不起作用
      • 代码本身没有问题。不了解枚举器的工作原理是一个问题。它可以通过在显示时锁定枚举器并在每次显示之前获取一个新的枚举器来解决。
      【解决方案5】:

      列表有一个内部版本计数器,当您更改列表内容时会更新。枚举器会跟踪版本并在发现列表发生更改时抛出异常。

      当您只是循环列表时,没有任何东西可以跟踪版本,因此没有任何东西可以捕捉到列表已更改。

      如果您在循环时更改列表,您可能会得到不需要的效果,这是枚举器保护您免受的影响。例如,如果您从列表中删除一个项目而不更改循环索引,以便它仍然指向同一个项目,您可能会错过循环中的项目。同样,如果您在不更正索引的情况下插入项目,您可能会多次迭代同一个项目。

      【讨论】:

        【解决方案6】:

        您无法在枚举时更改集合。

        问题是您在集合未满时开始枚举,并尝试在枚举时继续添加项目

        【讨论】:

          【解决方案7】:

          代码有缺陷,您睡了 5 秒钟,但并非所有项目都已添加到列表中。这意味着您在第一个线程完成将项目添加到列表之前开始在一个线程上显示项目,从而导致基础集合更改并使枚举器无效。

          从 Add 代码中删除 Thread.Sleep 突出显示了这一点:

          public static void AddItems(object state_)
          {     
             for (int i = 1; i <= 50; i++)      
             {        
                 _collection.Add(i.ToString());      
                 Console.WriteLine("Adding " + i);  
             }   
          } 
          

          您应该使用同步机制来等待第一个线程完成添加项目的工作,而不是休眠。

          【讨论】:

          • 这与睡眠机制无关。问题在于您在代码 sn-p 下方列出的同步。 (所以我删除了反对票)
          • @Chris Ballance:如果你看看我的回答,我会解释为什么发布的代码与描述的一样。我清楚地说明了为什么枚举器变得无效并抛出异常。有时 SO 很疯狂!
          • 顺便说一句,我并不是说从 Add 循环中删除 Thread.Sleep() 是一种解决方法!!
          猜你喜欢
          • 2021-03-08
          • 1970-01-01
          • 2012-11-08
          • 2011-11-01
          • 1970-01-01
          • 2015-01-23
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多