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


90

如果我使用:

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

Addforeach引发InvalidOperationException(集合被修改;枚举操作可能不会执行),我认为合理的,因为我们从我们脚下拉地毯。

但是,如果我使用:

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

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

这让我感到意外,因为我一直以为List.ForEach只是for foreach或for 的包装for
是否有人对此行为的方式和原因做出了解释?

(由ForEach循环为泛型列表不断重复标记)


7
我同意。这是可疑的。我想请您将其发布在Microsoft连接上,并要求进行澄清。
TomTom

4
“这让我感到意外,因为我一直认为List.ForEach只是foreach或的包装for。” 它仍然可以使用for。您可以在for循环中执行相同的操作,并生成相同的OutOfMemoryException。
安东尼·佩格拉姆

这是基于我的问题:stackoverflow.com/q/9311272/132239,感谢SWeko深入介绍了它的详细信息
Kasrak

Answers:


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
在添加方法-> this._items [this._size ++] = item;处刷新。
法比奥(Fabio)2012年

1
@SWeko,它不会计算,每次添加或删除项目时都会更新。
Thomas Levesque 2012年

1
由于存在针对更改列表本身的操作的更新,因此其中有一个_version私有变量List<T>可以检测到这种情况。
SWeko 2012年

您可以通过先获取大小(int theSize = this._size),然后在for循环中使用它来避免异常?
Lazlow 2012年


6

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

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

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


并回答@Thomas帖子中有关其刷新方式的评论-调用add时内部成员会刷新,这就是为什么它能够跟上更改的原因。如果要执行插入操作,并且索引要比当前索引小,那么您将永远不会对该项目进行操作,因为它已经遍历了该项目。但是,由于您要添加到最后,所以它可以工作。
Mike Perrenoud 2012年

1
是的,只需更改Addstrings.Insert(0, s + "!")即可打印出“样本”。奇怪的是,文档中根本没有提到这一点。
SWeko 2012年

好吧,我认为Microsoft意识到提供文档中存在的所有警告几乎是不可能的-因此他们现在提供了源代码。我确实找到了一个更好的解决方案,但我发现的唯一问题是WF之类的产品无法尽快更新-4.x WF源代码仍然不可用。
Mike Perrenoud 2012年

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中。

David Kean(BCL团队)


1
我看到这将是一个重大的重大突破性变化,但是尽管如此,它仍可能以非显而易见的方式失败,而这绝不是一件好事。我看不到使用ForEach方法优于简单方法的情况(如果不需要修改原始列表,则使用ForEach方法)
SWeko 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.