循环中的 Lambda 变量捕获 - 这里会发生啥? [复制]

Posted

技术标签:

【中文标题】循环中的 Lambda 变量捕获 - 这里会发生啥? [复制]【英文标题】:Lambda variable capture in loop - what happens here? [duplicate]循环中的 Lambda 变量捕获 - 这里会发生什么? [复制] 【发布时间】:2012-10-12 14:46:27 【问题描述】:

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

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

谢谢。

【问题讨论】:

blogs.msdn.com/b/ericlippert/archive/2009/11/12/… 看来VS2013和.NET 4.5已经不是问题了 【参考方案1】:

在这一行

 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

【讨论】:

我在我的回答中链接了它,但由于这个可能最终会在投票列表的顶部:blogs.msdn.com/b/ericlippert/archive/2009/11/12/… C# 5.0 于 2012 年 8 月发布,它包含在 Visual Studio 2012 中,因此从技术上讲它已经“修复”了。 @PaoloMoretti:还是更好地等待服务包 1? :)【参考方案2】:

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

听起来像你期望的那样:

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));

这清楚地表明您在每次迭代中都捕获了 same 变量。当您稍后实际执行该代码时,它们都引用了已递增到 10 的 same 值。

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

【讨论】:

我认为对于新程序员来说,另一个常见的误解不是 foreach 生成不同的变量,而是 lambdas 关闭值而不是变量。 @Servy - 对于引用类型,在大多数情况下实际上是一样的。 嗯,不,真的,不是。如果您从未更改变量的值以引用另一个对象(就像您从未将值类型更改为另一个值一样),那么关闭变量或值都没关系。除非变量是只读的(或者至少在创建闭包和调用闭包之间没有改变),否则会有区别。此外,这是处理值类型,而不是引用类型。【参考方案3】:

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

【讨论】:

以上是关于循环中的 Lambda 变量捕获 - 这里会发生啥? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

捕获的变量实例化问题

同步不在循环中的线程会发生啥

如果 lambda 在运行时被移动/破坏会发生啥?

iOS 中Block的理解以及啥时候会引起循环引用

Python lambda捕获外部变量

c++11 lambdas 捕获他们不使用的变量吗?