Lambda捕获问题与迭代器?

时间:2011-08-19 15:28:50

标签: c# lambda scope closures

如果已经提出这个问题,请道歉,但假设我们有这个代码(我用Mono 2.10.2运行它并用gmcs 2.10.2.0编译):

using System;

public class App {
    public static void Main(string[] args) {
        Func<string> f = null;
        var strs = new string[]{
            "foo",
            "bar",
            "zar"
        };

        foreach (var str in strs) {
            if ("foo".Equals(str)) 
                f = () => str;
        }
        Console.WriteLine(f());     // [1]: Prints 'zar'

        foreach (var str in strs) {
            var localStr = str;
            if ("foo".Equals(str))
                f = () => localStr;
        }
        Console.WriteLine(f());     // [2]: Prints 'foo'

        { int i = 0;
        for (string str; i < strs.Length; ++i) {
            str = strs[i];
            if ("foo".Equals(str)) 
                f = () => str;
        }}
        Console.WriteLine(f());     // [3]: Prints 'zar'
    }
}

[1]打印与[3]相同似乎是合乎逻辑的。但说实话,我不知何故预计它打印的内容与[2]相同。我不知何故相信[1]的实施将更接近[2]

问题:任何人都可以提供对规范的引用,它可以准确地告诉str变量(或者甚至是迭代器)是如何被{{1}中的lambda捕获的}。

我想我正在寻找的是[1]循环的确切实现。

4 个答案:

答案 0 :(得分:11)

您要求提供规范;相关位置是第8.8.4节,其中指出“foreach”循环等同于:

    V v;
    while (e.MoveNext()) {
        v = (V)(T)e.Current;
        embedded-statement
    }

请注意,值v在while循环外声明,因此存在单个循环变量。然后由lambda关闭。

更新

因为有很多人遇到这个问题,C#设计和编译团队改变了C#5以具有这些语义

    while (e.MoveNext()) {
        V v = (V)(T)e.Current;
        embedded-statement
    }

然后具有预期的行为 - 每次都关闭不同的变量。从技术上讲,这是一个突破性的变化,但是依赖于您正在经历的奇怪行为的人数希望非常小。

请注意,在这方面,C#2,3和4现在与C#5不兼容。另请注意,此更改仅适用于foreach,而不适用于for循环。

有关详细信息,请参阅http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/


Commenter abergmeier说:

  

C#是唯一具有这种奇怪行为的语言。

此声明绝对错误。请考虑以下JavaScript:

var funcs = [];
var results = [];
for(prop in { a : 10, b : 20 })
{
  funcs.push(function() { return prop; });
  results.push(funcs[0]());
}

像异常,您是否想要猜测results的内容是什么?

答案 1 :(得分:2)

1/3和2之间的核心差异是正在捕获的变量的生命周期。在1和3中,lambda捕获迭代变量str。在forforeach循环中,循环的生命周期中有一个迭代变量。当lambda在循环结束时执行时,它将使用最终值zar

执行

在2中,您捕获的是一个局部变量,其生命周期是循环的单次迭代。因此,您捕获当时的值“foo”

我能告诉你的最好的参考是Eric关于这个主题的博客文章

答案 2 :(得分:0)

以下内容发生在循环1和3中:

将当前值分配给变量str。它始终是相同的变量,每次迭代中只有不同的值。该变量由lambda捕获。由于lambda在循环结束后执行,因此它具有数组中最后一个元素的值。

以下内容发生在循环2中:

将当前值分配给新变量localStr。它始终是一个获取分配值的新变量。这个新变量由lambda捕获。因为循环的下一次迭代会创建一个新变量,所以捕获变量的值不会改变,因此会输出“foo”。

答案 3 :(得分:0)

来自谷歌的人

我已经使用这种方法修复了lambda bug:

我已经改变了这个

for(int i=0;i<9;i++)
    btn.OnTap += () => { ChangeCurField(i * 2); };

到这个

for(int i=0;i<9;i++)
{
    int numb = i * 2;
    btn.OnTap += () => { ChangeCurField(numb); };
}

这迫使麻木&#34;变量是lambda的唯一变量,并且此时也生成,而不是在调用/生成lambda时生成&lt;不知道什么时候发生。