【问题标题】:In C#, why can't an anonymous method contain a yield statement?在 C# 中,为什么匿名方法不能包含 yield 语句?
【发布时间】:2012-09-14 02:50:41
【问题描述】:

我认为做这样的事情会很好(使用 lambda 进行收益返回):

public IList<T> Find<T>(Expression<Func<T, bool>> expression) where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();

    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

但是,我发现我不能在匿名方法中使用 yield。我想知道为什么。 yield docs 只是说不允许。

由于不允许,我只创建了 List 并将项目添加到其中。

【问题讨论】:

  • 现在我们可以在 C# 5.0 中使用匿名 async lambdas 允许 await 内部,我很想知道为什么他们仍然没有实现带有 yield 内部的匿名迭代器。或多或少,它是同一个状态机生成器。

标签: c# yield anonymous-methods yield-return


【解决方案1】:

Eric Lippert 最近撰写了一系列博客文章,介绍为什么在某些情况下不允许使用收益。

EDIT2:

  • Part 7(这个是后来发的,专门解决这个问题的)

你可能会在那里找到答案......


EDIT1:这在第 5 部分的 cmets 中进行了解释,在 Eric 对 Abhijeet Patel 评论的回答中:

问:

埃里克,

您能否提供一些见解 为什么“产量”不允许在 匿名方法或 lambda 表达式

一个:

好问题。我很想拥有 匿名迭代器块。这将是 能够建造真是太棒了 自己一个小序列发生器 就地关闭本地 变量。没有的原因是 直截了当:好处不 超过成本。的厉害 就地制作序列生成器是 实际上在盛大的时候很小 事物方案和名义方法 在大多数情况下做得足够好 情景。所以好处不 很有说服力。

成本很高。迭代器 重写是最复杂的 编译器中的转换,以及 匿名方法重写是 第二个最复杂的。匿名的 方法可以在其他匿名内部 方法,匿名方法可以是 在迭代器块内。所以, 我们所做的是首先我们重写所有 匿名方法,使它们成为 闭包类的方法。这是 编译器的倒数第二件事 在为方法发出 IL 之前执行。 完成该步骤后,迭代器 rewriter 可以假设没有 迭代器中的匿名方法 堵塞;他们都被改写了 已经。因此迭代器 重写器可以专注于 重写迭代器,没有 担心可能会有 里面有未实现的匿名方法。

此外,迭代器块永远不会“嵌套”, 与匿名方法不同。迭代器 重写器可以假设所有迭代器 块是“顶级”。

如果允许匿名方法 包含迭代器块,然后两者 这些假设都消失了。 你可以有一个迭代器块 包含一个匿名方法 包含一个匿名方法 包含一个迭代器块 包含一个匿名方法,并且... 呸。现在我们要写一个重写 可以处理嵌套迭代器的传递 块和嵌套匿名方法 同时,合并我们两个最 复杂的算法合而为一 更复杂的算法。它会 真的很难设计,实施, 和测试。我们足够聪明 所以,我敢肯定。我们有一个聪明的团队 这里。但我们不想承担 一个“很高兴拥有”的巨大负担 但不是必需的”功能。-- Eric

【讨论】:

  • 很有趣,尤其是现在有本地函数了。
  • 我想知道这个答案是否已经过时,因为它会在本地函数中返回一个收益。
  • @Joshua 但是本地函数与匿名方法不同......匿名方法中仍然不允许返回。
【解决方案2】:

Eric Lippert 在iterator blocks 上撰写了一系列关于限制(以及影响这些选择的设计决策)的优秀文章

特别是迭代器块是由一些复杂的编译器代码转换实现的。这些转换会影响匿名函数或 lambda 内部发生的转换,因此在某些情况下,它们都会尝试将代码“转换”为与其他不兼容的其他构造。

因此他们被禁止互动。

迭代器块如何在后台工作得到很好的处理here

作为一个不兼容的简单示例:

public IList<T> GreaterThan<T>(T t)
{
    IList<T> list = GetList<T>();
    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

编译器同时希望将其转换为:

// inner class
private class Magic
{
    private T t;
    private IList<T> list;
    private Magic(List<T> list, T t) { this.list = list; this.t = t;}

    public IEnumerable<T> DoIt()
    {
        var items = () => {
            foreach (var item in list)
                if (fun.Invoke(item))
                    yield return item;
        }
    }
}

public IList<T> GreaterThan<T>(T t)
{
    var magic = new Magic(GetList<T>(), t)
    var items = magic.DoIt();
    return items.ToList();
}

同时迭代器方面正在尝试做一个小的状态机。某些简单的示例可能会进行大量的完整性检查(首先处理(可能是任意的)嵌套闭包),然后查看最底层的结果类是否可以转换为迭代器状态机。

然而这将是

  1. 相当多的工作。
  2. 如果至少迭代器块方面不能阻止闭包方面应用某些转换以提高效率(例如将局部变量提升为实例变量而不是完全成熟的闭包类),则不可能在所有情况下都工作。
    • 如果在不可能或很难不实施的情况下甚至有轻微的重叠可能性,那么导致的支持问题的数量可能会很高,因为许多用户会丢失细微的重大更改。
  3. 可以很容易地解决它。

在你的例子中是这样的:

public IList<T> Find<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    return FindInner(expression).ToList();
}

private IEnumerable<T> FindInner<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();
    foreach (var item in list)
        if (fun.Invoke(item))
            yield return item;
}

【讨论】:

  • 编译器在解除所有闭包后不能进行通常的迭代器转换,没有明确的原因。你知道一个实际上会带来一些困难的案例吗?顺便说一句,您的 Magic 课程应该是 Magic&lt;T&gt;
【解决方案3】:

不幸的是,我不知道他们为什么不允许这样做,因为当然完全有可能设想这将如何工作。

然而,匿名方法已经是“编译器魔法”的一部分,因为该方法将被提取到现有类中的方法,甚至是一个全新的类,这取决于它是否处理局部变量与否。

此外,使用yield 的迭代器方法也使用编译器魔法实现。

我的猜测是,这两个中的一个使代码无法被另一个魔术识别,并且决定不花时间为当前版本的 C# 编译器进行这项工作。当然,这可能根本不是一个有意识的选择,而且它只是行不通,因为没有人想过要实现它。

对于 100% 准确的问题,我建议您使用 Microsoft Connect 网站并报告问题,我相信您会得到有用的回报。

【讨论】:

    【解决方案4】:

    我会这样做:

    IList<T> list = GetList<T>();
    var fun = expression.Compile();
    
    return list.Where(item => fun.Invoke(item)).ToList();
    

    当然,对于 Linq 方法,您需要从 .NET 3.5 引用的 System.Core.dll。并包括:

    using System.Linq;
    

    干杯,

    狡猾

    【讨论】:

      【解决方案5】:

      也许它只是一个语法限制。在与 C# 非常相似的 Visual Basic .NET 中,虽然写起来很别扭,但完全可能

      Sub Main()
          Console.Write("x: ")
          Dim x = CInt(Console.ReadLine())
          For Each elem In Iterator Function()
                               Dim i = x
                               Do
                                   Yield i
                                   i += 1
                                   x -= 1
                               Loop Until i = x + 20
                           End Function()
              Console.WriteLine($"{elem} to {x}")
          Next
          Console.ReadKey()
      End Sub
      

      还要注意括号' here; lambda 函数 Iterator Function...End Function 返回 IEnumerable(Of Integer)不是这样的对象本身。必须调用它才能获取该对象。

      [1] 转换的代码在 C# 7.3 (CS0149) 中引发错误:

      static void Main()
      {
          Console.Write("x: ");
          var x = System.Convert.ToInt32(Console.ReadLine());
          // ERROR: CS0149 - Method name expected 
          foreach (var elem in () =>
          {
              var i = x;
              do
              {
                  yield return i;
                  i += 1;
                  x -= 1;
              }
              while (!i == x + 20);
          }())
              Console.WriteLine($"{elem} to {x}");
          Console.ReadKey();
      }
      

      我强烈不同意其他答案中给出的编译器难以处理的原因。您在 VB.NET 示例中看到的 Iterator Function() 是专门为 lambda 迭代器创建的。

      在VB中,有Iterator关键字;它没有 C# 对应物。恕我直言,没有真正的理由这不是 C# 的一个特性。

      因此,如果您真的非常想要匿名迭代器函数,目前使用 Visual Basic 或(我还没有检查过)F#,正如@Thomas Levesque 的回答中Part #7 的评论中所述(为 F# 执行 Ctrl+F )。

      【讨论】:

        猜你喜欢
        • 2015-04-26
        • 2013-04-30
        • 1970-01-01
        • 1970-01-01
        • 2011-06-25
        • 1970-01-01
        • 1970-01-01
        • 2011-03-21
        相关资源
        最近更新 更多