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

本文关键字:修改 许修改 列表 ForEach lt List gt 为什么 | 更新日期: 2023-09-27 18:24:45

如果我使用:

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

foreach中的Add抛出InvalidOperationException(Collection已被修改;枚举操作可能不会执行),我认为这是合乎逻辑的,因为我们正在从头开始。

但是,如果我使用:

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

它立即通过循环来射中自己的脚,直到抛出OutOfMemoryException。

这让我很惊讶,因为我一直认为List.ForEach只是foreachfor的包装器
有人能解释这种行为的方式和原因吗?

(由ForEach循环对无休止重复的泛型列表进行修复)

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

这是因为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在每次迭代时都会增加。

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

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

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

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

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

        var list = new List<string>();
        list.Add("Foo");
        list.Add("Bar");
        list.ForEach((item) => 
        { 
            if(item=="Foo") 
                list.Remove(item); 
        });

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

David Kean(BCL团队)