91

我认为做这样的事情会很好(使用 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 并将项目添加到其中。

4

5 回答 5

116

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

编辑2:

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

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


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

问:

埃里克,

您是否还可以提供一些关于为什么在匿名方法或 lambda 表达式中不允许“yield”的见解

A :

好问题。我希望有匿名迭代器块。能够就地构建一个封闭局部变量的小型序列生成器,这将是非常棒的。不这样做的原因很简单:收益不会超过成本。就地制作序列生成器的美妙之处实际上在宏伟的计划中非常小,而名义方法在大多数情况下都可以很好地完成这项工作。因此,好处并不那么引人注目。

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

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

如果允许匿名方法包含迭代器块,那么这两个假设都将失效。您可以拥有一个包含匿名方法的迭代器块,该方法包含一个匿名方法,该匿名方法包含一个包含匿名方法的迭代器块,并且......糟糕。现在我们必须编写一个重写过程,它可以同时处理嵌套的迭代器块和嵌套的匿名方法,将我们两个最复杂的算法合并为一个复杂得多的算法。设计、实现和测试真的很难。我们很聪明,可以这样做,我敢肯定。我们这里有一个聪明的团队。但是我们不想为“拥有但不是必需的”功能承担如此大的负担。——埃里克

于 2009-08-01T23:21:38.253 回答
22

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;
}
于 2009-08-01T23:18:29.633 回答
5

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

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

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

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

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

于 2009-08-01T23:17:15.610 回答
1

我会这样做:

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;

干杯,

狡猾

于 2009-09-19T20:42:58.333 回答
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();
}

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

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

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

于 2018-10-08T15:20:33.150 回答