为啥 List<T>.ForEach 允许修改其列表?

Posted

技术标签:

【中文标题】为啥 List<T>.ForEach 允许修改其列表?【英文标题】:Why does List<T>.ForEach allow its list to be modified?为什么 List<T>.ForEach 允许修改其列表? 【发布时间】:2012-03-07 21:03:51 【问题描述】:

如果我使用:

var strings = new List<string>  "sample" ;
foreach (string s in strings)

  Console.WriteLine(s);
  strings.Add(s + "!");

foreach 中的 Add 抛出 InvalidOperationException(集合已修改;枚举操作可能无法执行),我认为这是合乎逻辑的,因为我们正在从脚下拉扯地毯。

但是,如果我使用:

var strings = new List<string>  "sample" ;
strings.ForEach(s =>
  
    Console.WriteLine(s);
    strings.Add(s + "!");
  );

它会迅速循环,直到抛出 OutOfMemoryException。

这对我来说是一个惊喜,因为我一直认为 List.ForEach 要么只是 foreachfor 的包装器。 有没有人解释这种行为的方式和原因?

(灵感来自ForEach loop for a Generic List repeated endlessly)

【问题讨论】:

我同意。这是 - 可疑的。我建议您将其发布在 microsoft connect 上并要求澄清。 "这对我来说是一个惊喜,因为我一直认为 List.ForEach 要么只是 foreachfor 的包装器。"它仍然可以使用for。您可以在 for 循环中执行相同的操作并生成相同的 OutOfMemoryException 作为结果。 这是基于我的问题:***.com/q/9311272/132239,感谢 SWeko 了解它的详细信息 【参考方案1】:

这是因为ForEach 方法不使用枚举器,它使用for 循环遍历项目:

public void ForEach(Action<T> action)

    if (action == null)
    
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    
    for (int i = 0; i < this._size; i++)
    
        action(this._items[i]);
    

(使用 JustDecompile 获得的代码)

由于没有使用枚举器,它永远不会检查列表是否发生了变化,并且永远不会达到for 循环的结束条件,因为每次迭代都会增加_size

【讨论】:

是的,但是_size 是如何计算的?如果它只是预先计算的,那么对于我的示例,如果应该只运行一次。它显然以某种方式刷新了。 在Add方法刷新-> this._items[this._size++] = item; @SWeko,不是计算的,每次添加或删除项目时都会更新。 List&lt;T&gt; 中有一个 _version 私有变量可以检测到这种情况,因为它会在更改列表本身的操作上更新。 您可以通过首先获取大小 (int theSize = this._size),然后在 for 循环中使用它来避免异常?【参考方案2】:

List&lt;T&gt;.ForEach内部是通过for实现的,所以它不使用枚举器,它允许修改集合。

【讨论】:

【参考方案3】:

因为附加到 List 类的 ForEach 在内部使用了一个直接附加到其内部成员的 for 循环 - 您可以通过下载 .NET 框架的源代码来查看。

http://referencesource.microsoft.com/netframework.aspx

foreach 循环首先是编译器优化,但还必须作为观察者对集合进行操作——因此,如果集合被修改,它会引发异常。

【讨论】:

并回答@Thomas 帖子上关于它如何刷新的评论——调用 add 时会刷新内部成员,这就是它能够跟上变化的原因。如果您要在小于当前索引的索引处执行插入,您将永远不会对该项目进行操作,因为它已经迭代过该项目。但是,由于您要添加到最后,所以它可以工作。 是的,将Add 行更改为strings.Insert(0, s + "!") 只会打印出“样本”。奇怪的是,文档中根本没有提到这一点。 嗯,我认为微软意识到提供文档中存在的所有警告几乎是不可能的——所以他们现在提供了源代码。老实说,我发现一个更好的解决方案,但我发现的唯一问题是 WF 之类的产品更新速度不快—— 4.x WF 源代码仍然不可用。【参考方案4】:

我们知道这个问题,这是最初编写时的疏忽。不幸的是,我们无法更改它,因为它现在会阻止以前工作的代码运行:

        var list = new List<string>();
        list.Add("Foo");
        list.Add("Bar");

        list.ForEach((item) => 
         
            if(item=="Foo") 
                list.Remove(item); 
        );

正如Eric Lippert 指出的那样,这种方法本身的实用性值得怀疑,因此我们没有将它包含在用于 Metro 风格应用程序(即 Windows 8 应用程序)的 .NET 中。

大卫·基恩(BCL 团队)

【讨论】:

我知道这将是一个非常糟糕的重大变化,但它可能会以不明显的方式失败,这绝不是一件好事。我看不到使用 ForEach 方法优于简单 for 的场景(如果不需要修改原始列表,则为 foreach)

以上是关于为啥 List<T>.ForEach 允许修改其列表?的主要内容,如果未能解决你的问题,请参考以下文章

List<T> 将 foreach 循环中的所有项目覆盖为最后一个值

mybatis 的 foreach 使用hashmap 传输 List ,hashmap中的List 不空,List中的元素为空,为啥呢。

为啥我在 List<List<T>> 中复制 List<T> 时存在依赖关系? [复制]

为啥 List<T> 在 .NET 4.5 中实现 IReadOnlyList<T>?

C#中如何用for循环遍历List<类>?

为啥 List<T> 的“索引超出范围”异常,但数组却没有?