5

我正在努力解决问题,这里会发生什么?编译器生成什么样的代码?

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。有人可以给我一些编译器在这里做什么的伪代码吗?

谢谢。

4

3 回答 3

10

在这一行

 listActions.Add(() => Console.WriteLine(i));

变量i, 被捕获,或者如果您愿意,可以创建一个指向该变量的内存位置的指针。这意味着每个委托都有一个指向该内存位置的指针。在这个循环执行之后:

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

显而易见的原因i是,因此(s) 中10存在的所有指针所指向的内存内容变为 10。Action

换句话说,i被捕获。

顺便说一句,应该注意的是,根据 Eric Lippert的说法,这种“奇怪”的行为将 在C# 5.0.

因此,在C# 5.0您的程序中将按预期打印:

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

编辑:

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

重新审视循环中的闭包

于 2012-10-12T14:50:34.220 回答
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 的预期那样工作

于 2012-10-12T14:55:33.657 回答
0

本质上,正在发生的事情是编译器将您声明int i在循环之外,因此不会为每个操作创建一个新的值,而只是每次都引用同一个。当你执行代码时,我是 10,所以它打印了很多 10。

于 2012-10-12T14:51:13.793 回答