为什么List<T>.ForEach允许修改列表?

89

如果我使用:

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 + "!");
  });

使用List.ForEach时,它会循环直到抛出OutOfMemoryException异常,这会让人感到惊讶,因为我一直以为List.ForEach只是foreach或for的包装器。
是否有人能解释一下这种行为的原因和方式?

(参考ForEach loop for a Generic List repeated endlessly)


7
我同意。这有些可疑。我建议您将其发布在Microsoft Connect上并要求澄清。 - TomTom
4
这让我感到惊讶,因为我一直认为List.ForEach只是foreachfor的包装器之一。但实际上仍可以使用for来执行相同的操作,并且可能会导致同样的OutOfMemoryException异常。 - Anthony Pegram
这是基于我的问题:http://stackoverflow.com/q/9311272/132239,感谢SWeko深入了解它的细节。 - Kasrak
4个回答

68

这是因为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是如何计算的呢?如果它只是预先计算好的,那么对于我的示例来说它应该只运行一次。显然它会以某种方式被刷新。 - SWeko
7
在 Add 方法中进行了刷新 -> this._items[this._size++] = item; - Fabio
1
@SWeko,它不是计算出来的,而是每次添加或删除项目时更新。 - Thomas Levesque
1
List<T>中有一个名为_version的私有变量,它可以检测到这种情况,因为它会在更改列表本身的操作中进行更新。 - SWeko
您可以通过先获取大小(int theSize = this._size),然后在for循环中使用它来避免异常吗? - Lazlow

14
List<T>.ForEach是通过内部的for实现的,因此它不使用枚举器并允许修改集合。

6

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

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

而foreach循环首先是编译器优化,但也必须作为观察者针对集合进行操作 - 因此,如果修改了集合,则会引发异常。


回答@Thomas帖子中关于它如何刷新的评论 - 当调用add时,内部成员会被刷新,因此它能够跟上更改。如果您要执行插入操作,并且索引小于当前索引,则永远不会对该项进行操作,因为已经迭代过该项。但由于您是添加到末尾,所以可以正常工作。 - Mike Perrenoud
1
是的,将Add行更改为strings.Insert(0, s + "!")只会打印出“sample”。奇怪的是,文档中根本没有提到这一点。 - SWeko
我认为微软意识到在他们的文档中提供每一个细节是几乎不可能的,所以现在他们提供源代码。说实话,我觉得这是更好的解决方案,但唯一的问题是像WF这样的产品更新速度不够快——4.x WF源代码仍然不可用。 - Mike Perrenoud

4
我们知道这个问题,这是最初编写时的疏忽。不幸的是,我们不能更改它,因为这将阻止先前运行正常的代码。
        var list = new List<string>();
        list.Add("Foo");
        list.Add("Bar");

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

这种方法本身的效用有问题,正如Eric Lippert所指出的那样,因此我们没有在.NET for Metro风格应用程序(即Windows 8应用程序)中包含它。
David Kean(BCL团队)

1
我看到这将是一个重大的破坏性变更,但它仍然可能以非明显的方式失败,这从来不是一件好事。我无法想象使用ForEach方法优于简单的for循环(或foreach,如果不需要操纵原始列表)的情况。 - SWeko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接