【问题标题】:Lambda capture problem with iterators?迭代器的 Lambda 捕获问题?
【发布时间】:2011-10-30 17:38:08
【问题描述】:

抱歉,如果这个问题已经被问过,但假设我们有这段代码(我用 Mono 2.10.2 运行它并用 gmcs2.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]

问题:任何人都可以提供对规范的参考,其中准确地说明了 [1] 中的 lambda 是如何捕获 str 变量(或者甚至是迭代器)的。 p>

我想我正在寻找的是 foreach 循环的确切实现。

【问题讨论】:

    标签: c# lambda scope closures


    【解决方案1】:

    在循环 1 和 3 中发生以下情况:

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

    在循环 2 中发生以下情况:

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

    【讨论】:

      【解决方案2】:

      1 / 3 和 2 之间的核心区别在于被捕获变量的生命周期。在 1 和 3 中,lambda 正在捕获迭代变量 str。在forforeach 循环中,循环的生命周期都有一个迭代变量。当 lambda 在循环结束时执行时,它会以最终值执行:zar

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

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

      【讨论】:

        【解决方案3】:

        您要求提供对规范的参考;相关位置是第 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/


        评论者 abergmeier 说:

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

        此说法绝对错误。考虑以下 JavaScript:

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

        abergmeier,你愿意猜猜results 的内容是什么吗?

        【讨论】:

        • 第一个实现与for 循环头部中声明的变量的通常范围一致。但是新的实现试图适应这样一个事实,即人类期望并依赖于不一致的行为(这可能导致难以发现错误——它发生在我身上)。嗯,老实说,我对这个决定有复杂的感觉......
        • @sinharaj:我也有复杂的感觉。一致性很好,因为它有助于你的直觉。但在这种情况下,一致性违背人们对预期行为的直觉。人们不认为foreach的循环变量是一个变量,他们认为它是一个。 (而且你不能把它当作一个变量;例如,你不能自己写它。)我不想有一种“愚蠢的一致性”,它造成的伤害多于它所防止的伤害。
        • @Eric:我投票赞成改变。恕我直言,现在的方式与直觉相反。捕获变量的 lambda 在循环内声明,因此人们希望它在那个时间点具有值。然而,这只是一大堆相关问题中的一部分。 var i=0; Action a = () =&gt; Debug.Write(i); i = 2; a(); 这里发生了与 i 相同的反直觉变化。我不知道在一个地方改变这种行为是否好,但把它留在另一个地方。
        • @Daniel:但是这样想:var city = "London"; var q = from c in customers where c.City == city select c; Console.WriteLine(q.First()); city = "Manchester"; Console.WriteLine(q.First());——人们期望这会产生两种不同的结果。 lambda 对变量而不是值是封闭的,它应该观察变量的最新状态。
        • @Eric:你真的期待这个吗?我不太确定这一点。如果您不知道查询使用延迟执行,则不会。甚至:一旦执行完毕,为什么要突然改变结果?当我开始使用 LINQ 和 lambdas 时,我确实发现了这种奇怪的事情。
        【解决方案4】:

        谷歌用户

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

        我改变了这个

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

        这会强制“numb”变量成为 lambda 的唯一变量,并且在此时生成,而不是在调用/生成 lambda 时生成

        【讨论】:

          猜你喜欢
          • 2012-04-15
          • 1970-01-01
          • 2012-07-18
          • 2011-02-13
          • 2011-02-15
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多