繁体   English   中英

为什么枚举通过集合抛出异常但循环遍历其项目却没有

[英]Why does enumerating through a collection throw an exception but looping through its items does not

我正在测试一些同步结构,我注意到一些让我感到困惑的事情。 当我在同时写入一个集合时枚举它时,它抛出了异常(这是预期的),但是当我使用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);
        }
    }
}

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

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

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

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

回答你的实际问题......

枚举时,您将获得一个绑定到列表状态的IEnumerator,就像您要求它一样。 进一步的操作在枚举器上运行(MoveNext,Current)。

当使用for循环时,如果通过索引获取特定项目的调用,则会生成序列。 没有外部上下文,例如枚举器,它知道你在循环中。 对于所有收藏品都知道,你只需要一件物品。 由于该集合从未发布过调查员,因此无法知道您要求项目0,然后是项目1,然后是项目2等的原因是因为您正在查看列表。

如果你在走路的同时捣乱列表,那么无论如何都会出现错误。 如果添加项目,那么for循环可以静默跳过一些,而foreach循环将抛出。 如果删除项目,那么如果你运气不好,for循环可能会使索引超出范围,但大部分时间都可能正常工作。

但我认为你理解这一切,你的问题只是为什么两种迭代方式表现不同。 答案就是在一种情况下调用GetEnumerator时,以及在另一种情况下调用get_Item时,集合的状态(对于集合)是已知的。

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

在开始显示功能时获取一个新的枚举器,并在此过程中锁定列表。 或者,将List的深层副本复制到新的_displayCollection列表中并通过此单独的集合进行枚举,除了在显示过程开始之前填充之外,该集合将不会被写入。 希望这可以帮助。

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

枚举列表时,您枚举的是项目,而不是索引。 因此,在枚举时添加项目时,会使枚举失效。 它的构建方式可以防止你正在做的事情,你可以在列表6中插入一个项目的同时枚举列表中的第6项,在那里你可以枚举旧项目或新项目,或者一些未定义的状态。

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

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

当您只是循环列表时,没有任何内容可以跟踪版本,因此没有任何内容可以捕获列表已更改。

如果在循环播放时更改列表,则可能会产生不需要的效果,这是枚举器保护您的效果。 例如,如果您从列表中删除项目而不更改循环索引以使其仍指向同一项目,则可能会错过循环中的项目。 同样,如果在不更正索引的情况下插入项目,则可以多次迭代同一项目。

您无法通过它来更改集合。

问题是,当你的集合不完整时,你开始枚举,并尝试在枚举时保持添加项目

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

从添加代码中删除Thread.Sleep会突出显示:

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM