在C#中,为什么匿名方法不能包含yield语句?


87

我认为做这样的事情会很好(用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。我想知道为什么。该产量的文档只是说,这是不允许的。

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


既然我们可以在C#5.0中async允许匿名lambdaawait进入内部,我很想知道为什么他们仍然没有使用yield内部实现匿名迭代器。或多或少,它是相同的状态机生成器。
noseratio 2014年

Answers:


113

埃里克·利珀特(Eric Lippert)最近写了一系列博客文章,介绍为何在某些情况下不允许收益。

编辑2:

  • 第7部分 (稍后发布,专门解决这个问题)

您可能会在这里找到答案...


编辑1:这在第5部分的评论中得到解释,在埃里克对阿披耶特·帕特尔的评论的回答中:

问:

埃里克

您是否还可以提供一些见解,以了解为什么在匿名方法或lambda表达式中不允许使用“ yields”

A :

好问题。我很想拥有匿名迭代器块。能够就地构建一个封闭局部变量的序列生成器,真是太棒了。不这样做的原因很简单:收益不超过成本。实际上,在宏伟的事情方案中,就地放置序列生成器的难度很小,而名义方法在大多数情况下都能很好地完成工作。因此,收益并不那么引人注目。

费用很大。迭代器重写是编译器中最复杂的转换,而匿名方法重写则是第二复杂的转换。匿名方法可以在其他匿名方法内部,而匿名方法可以在迭代器块内部。因此,我们要做的是首先重写所有匿名方法,使它们成为闭包类的方法。这是编译器在为方法发出IL之前要做的第二件事。完成该步骤后,迭代器重写器可以假定迭代器块中没有匿名方法;它们都已经被重写了。因此,迭代器重写器可以只专注于重写迭代器,而不必担心其中可能存在未实现的匿名方法。

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

如果允许匿名方法包含迭代器块,则这两个假设都不在话下。您可以拥有一个迭代器块,其中包含一个匿名方法,该匿名方法包含一个匿名方法,该匿名方法包含一个包含一个匿名方法的迭代器块,然后... y。现在,我们必须编写一个重写通道,该通道可以同时处理嵌套的迭代器块和嵌套的匿名方法,将两个最复杂的算法合并为一个复杂得多的算法。设计,实施和测试真的很困难。我敢肯定,我们足够聪明。我们这里有一个聪明的团队。但是我们不想为“具有必要但并非必需的”功能承担这么大的负担。-埃里克


2
有趣,尤其是因为现在有本地功能。
马菲

4
我想知道这个答案是否过时,因为它将在局部函数中返回收益。
约书亚

2
@Joshua,但是局部函数与匿名方法不同...在匿名方法中仍然不允许收益返回。
托马斯·列维斯克

21

埃里克·利珀特(Eric Lippert)就迭代器块的局限性(以及影响这些选择的设计决策)撰写了一系列精彩的文章

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

结果,它们被禁止交互。

这里很好地解决了迭代器块如何在后台工作的问题。

作为不兼容的简单示例:

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

2
没有明确的理由说明,一旦编译器解除了所有闭包,编译器就无法进行常规的迭代器转换。您是否知道实际会遇到困难的情况?顺便说一句,您的Magic课程应该是Magic<T>
Qwertie

3

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

但是,从某种意义上说,匿名方法将被提取到现有类中的方法中,甚至提取到一个全新的类中,这取决于它是否处理局部变量,从某种意义上说,匿名方法已经是“编译器魔术”。

此外,使用 yield也可以通过编译器魔术来实现。

我的猜测是,这两者之一使代码无法与另一魔术结合使用,因此决定不花时间在当前版本的C#编译器上完成这项工作。当然,这可能根本不是一个明智的选择,并且因为没有人考虑实现它而只是行不通。

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


1

我会这样做:

IList<T> list = GetList<T>();
var fun = expression.Compile();

return list.Where(item => fun.Invoke(item)).ToList();

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

using System.Linq;

干杯,

狡猾


0

也许只是语法限制。在与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();
}

我非常不同意其他答案中给出的原因,即编译器难以处理。在Iterator Function()你的VB.NET例子中看到的是专门为拉姆达创建的迭代器。

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

因此,如果您真的想要匿名迭代器函数,请使用Visual Basic或(我没有检查过)F#,如@Thomas Levesque答案的第7部分注释中所述(对F#执行Ctrl + F)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.