【问题标题】: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 作为结果。
  • 这是基于我的问题:stackoverflow.com/q/9311272/132239,感谢 SWeko 了解它的详细信息

标签: c# list foreach


【解决方案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)
    猜你喜欢
    • 2011-02-24
    • 2010-10-19
    • 1970-01-01
    • 2017-03-19
    • 2011-10-28
    • 2012-07-14
    • 1970-01-01
    • 2010-11-01
    相关资源
    最近更新 更多