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

我想试试看,这里发生了什么? 编译器生成什么样的代码?

public static void vc() { var listActions = new List(); 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。 有人能给我一些编译器在这里做的伪代码吗?

谢谢。

在这一行

  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关于主题的post,但这是另一个:

重新审视循环中的闭包

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

这听起来像你期望的那样:

 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 。

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