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

时间:2012-10-12 14:46:28

标签: c# .net lambda

我正试图解决这个问题,这里发生了什么?编译器生成什么样的代码?

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

根据this,将为每次迭代创建一个新的ActionHelper实例。那么在这种情况下,我认为它应该打印1..10。 有人能给我一些编译器在这里做的伪代码吗?

感谢。

3 个答案:

答案 0 :(得分: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关于主题的帖子,但这是另一个:

Closure in a Loop Revisited

答案 1 :(得分: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的相同的值。

这是编译器或运行时中的错误......这是语言设计团队的一个有意识的决定。但是,由于对其工作原理的困惑,他们已经推翻了这个决定并decided to risk a breaking change to make it work more like you expect for C# 5

答案 2 :(得分:0)

本质上发生的事情是编译器在循环之外声明你的int i,因此没有为每个动作创建它的新值,每次只引用相同的动作。当你执行代码时,我是10,所以它会打印10个。