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

95

我认为做这样的事情会很好(使用lambda进行yield return):

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 lambda允许内部使用await,我很想知道为什么他们仍然没有实现带有yield的匿名迭代器。或多或少,这是相同的状态机生成器。 - noseratio - open to work
5个回答

120
Eric Lippert最近写了一系列关于为什么在某些情况下不允许使用yield的博客文章。

编辑2:

  • Part 7 (这篇文章是后来发布的,专门回答了这个问题)

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


编辑1: 在Eric对Abhijeet Patel的评论的回答中,在第5部分的评论中有详细解释:

Q :

Eric,

你能否提供一些关于为什么匿名方法或lambda表达式中不允许使用“yield”的见解

A :

很好的问题。我很想要有匿名迭代器块。能够在本地变量上进行闭包生成小序列发生器会非常棒。不允许的原因很简单:收益不大。在现实情况下,在场景中进行现场生成序列发生器的可行性效果相对较小。

事实上,scheme of things和nominal methods在大多数情况下都能够胜任工作。因此,好处并不十分明显。

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

另外,迭代器块永远不会“嵌套”,不像匿名方法。迭代器重写器可以假定所有迭代器块都是“顶级”的。

如果允许匿名方法包含迭代器块,则这两个假设都无效了。您可以拥有一个迭代器块,其中包含一个匿名方法,该匿名方法包含一个匿名方法,该匿名方法包含一个迭代器块,该迭代器块包含一个匿名方法,等等。现在我们必须编写一个重写通道,可以同时处理嵌套的迭代器块和嵌套的匿名方法,将我们的两个最复杂的算法合并成一个更加复杂的算法。设计、实现和测试都将非常困难。我相信我们足够聪明,可以做到这一点。我们团队很聪明。但是,我们不想为“好有但不必要”的功能承担如此沉重的负担。-- Eric


2
有趣,尤其是现在有本地函数了。 - Mafii
4
我不确定这个答案是否过时了,因为它在一个局部函数中使用了 yield return。 - Joshua
2
@Joshua,但是本地函数与匿名方法并不相同...在匿名方法中仍然不允许使用yield return。 - Thomas Levesque

24

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

6

很遗憾,我不知道为什么他们不允许这样做,因为当然可以设想这将如何工作。

然而,匿名方法已经是一种“编译器魔法”,意味着该方法将被提取到现有类的方法中,或者甚至提取到一个全新的类中,这取决于它是否涉及局部变量。

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

我猜其中一个原因是代码对另一个魔法部分不可识别,因此决定不花时间使其在当前版本的C#编译器中工作。当然,这可能根本不是一个有意识的选择,只是因为没有人想到去实现它。

如果您想得到100%准确的答案,建议您使用Microsoft Connect网站并报告问题,我相信您会得到有用的回复。


1
我会这样做:
IList<T> list = GetList<T>();
var fun = expression.Compile();

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

当然,您需要从.NET 3.5引用System.Core.dll以使用Linq方法。并且包括:
using System.Linq;

祝好,

Sly


1
也许只是语法的限制。在非常类似于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() ' here
        Console.WriteLine($"{elem} to {x}")
    Next
    Console.ReadKey()
End Sub

请注意括号' here; lambda函数Iterator Function...End Function返回IEnumerable(Of Integer),但本身不是这样的对象。必须调用它才能获得该对象,这就是在End Function后面加上()的原因。

[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 的评论所述(使用 Ctrl+F 查找 F#)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接