【问题标题】:Enumerating over lambdas does not bind the scope correctly?枚举 lambdas 不能正确绑定范围?
【发布时间】:2014-04-07 15:47:53
【问题描述】:

考虑以下 C# 程序:

using System;
using System.Linq;
using System.Collections.Generic;

public class Test
{
    static IEnumerable<Action> Get()
    {
        for (int i = 0; i < 2; i++)
        {
            int capture = i;
            yield return () => Console.WriteLine(capture.ToString());
        }
    }

    public static void Main(string[] args)
    {
        foreach (var a in Get()) a();
        foreach (var a in Get().ToList()) a();
    }
}

在 Mono 编译器下执行时(例如 Mono 2.10.2.0 - 粘贴到 here),它会写入以下输出:

0
1
1
1

这对我来说似乎完全不合逻辑。当直接迭代yield函数时,for循环的范围是“正确”(据我理解)使用的。但是当我首先将结果存储在列表中时,范围始终是最后一个操作?!

我可以假设这是 Mono 编译器中的一个错误,还是我遇到了 C# 的 lambda 和 yield-stuff 的神秘角落?

顺便说一句:当使用 Visual Studio 编译器(以及 MS.NET 或 mono 执行)时,结果是预期的 0 1 0 1

【问题讨论】:

  • 看起来像一个错误。我可以理解为什么它会是一个错误。因为它在一个迭代器块中,capture 最终被提升到一个新类型的字段来表示迭代器。然后评估闭包之后,关闭对迭代器的隐式引用并访问它的capture 字段。 MS 确保在迭代器块的转换之前而不是之后执行闭包转换,这就是它具有不同行为的原因。
  • 是的,这是 Mono 中的一个错误。如果您愿意,请随时向 Mono 团队报告。
  • 啊,非常感谢您的回答。同时,我使用 Mono 的最新 Beta 版进行了测试,它给出了与 MS 编译器相同的结果,所以我猜 Mono 团队已经遇到过这种情况 :)

标签: c# lambda mono yield


【解决方案1】:

@Armaron - .ToList() 扩展返回 T 类型的列表,因为 ToArray() 返回 T[],正如命名约定所暗示的那样,但我认为你的回应是正确的。

这听起来像是编译器的问题。我同意 Servy 的说法,这可能是一个错误,但是,您是否尝试过以下方法?

public class Test
{
    private static int capture = 0;    

    static IEnumerable<Action> Get()
    {
        for (int i = 0; i < 2; i++)
        {
            capture++;
            yield return () => Console.WriteLine(capture.ToString());
        }
    }
}

此外,您可能想尝试静态方法,因为您的函数是静态的,所以这可能会执行更准确的转换。

List<T> list = Enumerable.ToList(Get()); 

当调用 ToList() 时,它似乎没有对每个值执行一次迭代,而是:

return new List<T>(Get());

代码中每个的第二个对我来说在实现中没有意义,因为除非您需要向 List 对象添加/删除其他操作,否则它为什么是必要的或有益的。第一个非常有意义,因为您所做的只是遍历对象并执行相关操作。我的理解是,在转换期间通过执行整个迭代计算静态 IEnumerbale 对象范围内的整数,并且由于范围,该操作将 int 保留为静态 int。另外,请记住,IEnumerable 只是一个由实现 IList 的 List 实现的接口,并且可能包含内置转换的逻辑。

话虽如此,我有兴趣看到/听到您的发现,因为这是一篇有趣的帖子。我肯定会赞成这个问题。如果我所说的任何内容需要澄清,或者如果某些内容是错误的,请提出问题,尽管我对使用 IEnumerable 的 yield 关键字很有信心,但这是一个独特的问题。

【讨论】:

    【解决方案2】:

    我会告诉你为什么是0 1 1 1

    foreach (var a in Get()) a();
    

    在这里你进入 Get 并开始迭代:

    i = 0 => return Console.WriteLine(i);
    

    yield 随函数返回并执行函数,将 0 打印到屏幕上,然后返回 Get() 方法并继续。

    i = 1 => return Console.WriteLine(i);
    

    yield 带着函数返回并执行函数,将 1 打印到屏幕上,然后返回到Get() 方法并继续(才发现它必须停止)。

    但是现在,您不是在每个项目发生时对其进行迭代,而是在构建一个列表,然后对该列表进行迭代。

    foreach (var a in Get().ToList()) a();
    

    您所做的与上面不同,Get().ToList() 返回一个列表或数组(不确定哪个)。所以现在发生了:

    i = 0 => return Console.WriteLine(i);
    

    在你的Main() 函数中,你会在内存中得到以下信息:

    var i = 0;
    var list = new List
    {
        Console.WriteLine(i)
    }
    

    你回到Get()函数:

    i = 1 => return Console.WriteLine(i);
    

    返回到您的Main()

    var i = 1;
    var list = new List
    {
        Console.WriteLine(i),
        Console.WriteLine(i)
    }
    

    然后呢

    foreach (var a in list) a();
    

    会打印出1 1

    似乎忽略了您确保在返回函数之前封装了值。

    【讨论】:

    • 问题是为什么 Visual Studio 和 Mono 编译器的行为不同。正如 Eric Lippert 和 Servy 所指出的,这似乎是 Mono 的一个错误。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-06
    • 1970-01-01
    相关资源
    最近更新 更多