循环中的 Lambda 变量捕获 - 发生了什么?

6

我试图理解这里发生了什么?编译器会生成什么样的代码?

public static void vc()
{
    var listActions = new List<Action>();

    foreach (int i in Enumerable.Range(1, 10))
    {
        listActions.Add(() => Console.WriteLine(i));
    }

    foreach (Action action in listActions)
    {
        action();
    }
}

static void Main(string[] args)
{ 
  vc();
}

输出: 10 10 .. 10

根据这个链接,每一次迭代都会创建一个新的ActionHelper实例。所以在这种情况下,我认为它应该输出1..10。 有人能给我一些编译器在这里执行的伪代码吗?

谢谢。


3
http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx - L.B
1
似乎在使用VS2013和.NET 4.5时,这不再是一个问题。 - Martin Ingvar Kofoed Jensen
3个回答

10
在这行代码中
 listActions.Add(() => Console.WriteLine(i));

变量 i 被捕获,或者说创建了一个指向该变量内存位置的指针。这意味着每个委托都得到了指向该内存位置的指针。循环执行完毕后:

foreach (int i in Enumerable.Range(1, 10))
{
    listActions.Add(() => Console.WriteLine(i));
}

由于明显的原因,i10,因此所有指向 Action(s) 的指针所表示的内存内容都变成了 10。

换句话说,i 被捕获了。

顺便提一下,根据 Eric Lippert 的说法,这种“奇怪”的行为将在 C# 5.0 中得到解决。

因此,在 C# 5.0 中,您的程序将会像预期的那样打印:

1,2,3,4,5...10

编辑:

找不到Eric Lippert关于这个主题的帖子,但这里有另一个帖子:

循环中的闭包再探


1
我在我的回答中提供了链接,但由于这篇文章可能会成为投票列表的首选:http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx - Joel Coehoorn
C# 5.0在2012年8月发布,并包含在Visual Studio 2012中,因此从技术上讲它已经“修复”了。 - Paolo Moretti
@PaoloMoretti:还是等待服务包1? :) - Tigran

9

编译器会生成什么样的代码?

听起来你期望的是:

foreach (int temp in Enumerable.Range(1, 10))
{
    int i = temp;
    listActions.Add(() => Console.WriteLine(i));
}

这使用了每次迭代都不同的变量,因此在创建lambda时捕获的是该变量的值。实际上,您可以使用这个确切的代码并获得所需的结果。

但编译器实际上所做的更接近于这样:

int i;
foreach (i in Enumerable.Range(1, 10))
{
    listActions.Add(() => Console.WriteLine(i));
}

这表明你在每次迭代中捕获的是 同一个 变量。当你稍后执行该代码时,它们都引用已经增加到10的相同值。

这不是编译器或运行时的错误... 这是语言设计团队有意的决定。然而,由于对此工作方式的混淆,他们已经扭转了这个决定,并决定冒险进行破坏性更改,使其更像C# 5的预期


我认为对于新手程序员来说,另一个常见的误解不是foreach生成不同的变量,而是lambda闭合值而不是变量。 - Servy
@Servy - 对于引用类型,在大多数情况下,它们实际上是相同的东西。 - Joel Coehoorn
好的,实际上并不是这样。如果您从未更改变量的值以引用另一个对象(就像从未将值类型更改为另一个值一样),则无论您关闭变量还是值都没有关系。除非该变量是只读的(或者至少在创建闭包和调用它之间没有更改),否则会有所区别。此外,这涉及到值类型而不是引用类型。 - Servy

0
实际上,编译器将您的int i声明在循环之外,因此每次操作都不会创建新值,而只是引用相同的值。执行代码时,i已经是10,因此会多次打印出10。

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