代码之家  ›  专栏  ›  技术社区  ›  Lance Fisher

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

  •  80
  • Lance Fisher  · 技术社区  · 15 年前

    我认为做这样的事情会很好(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 说是不允许的。

    因为它是不允许的,所以我创建了列表并将项目添加到其中。

    5 回复  |  直到 6 年前
        1
  •  98
  •   Thomas Levesque    14 年前

    埃里克·利珀特最近写了一系列的博客文章,解释为什么在某些情况下不允许让步。

    编辑2:

    • Part 7 (这篇文章稍后发表,专门讨论这个问题)

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


    Edit1:这在第5部分的评论中解释,在埃里克对Abhijeet Patel评论的回答中:

    问:

    埃里克,

    你也能提供一些关于 为什么不允许在 匿名方法或lambda表达式

    答:

    问得好。我很想拥有 匿名迭代器块。它将会是 完全棒极了 你自己一个小序列发生器 在当地关闭的地方 变量。为什么不呢? 直截了当:好处不是 超过成本。令人敬畏的 使序列发生器就位是 实际上在大酒店里很小 事物方案和名义方法 大部分工作做得足够好 情节。所以好处不是 令人信服。

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

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

    如果允许匿名方法 包含迭代器块,然后两者都包含 那些假设是不可能的。 您可以有一个迭代器块, 包含一个匿名方法 包含一个匿名方法 包含一个迭代器块, 包含匿名方法,并且… 讨厌。现在我们必须重写 可以处理嵌套迭代器的传递 块和嵌套的匿名方法位于 同时,把我们两个最 将复杂的算法融合为一个整体 更复杂的算法。它会 很难设计、实现, 并进行测试。我们足够聪明 所以,我敢肯定。我们有一个聪明的团队 在这里。但我们不想承担 一个“很好拥有”的巨大负担 但不是必要的“特征。——埃里克

        2
  •  19
  •   joce    6 年前

    埃里克·利珀特写了一系列关于限制(以及影响这些选择的设计决策)的优秀文章 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;
    }
    
        3
  •  2
  •   Lasse V. Karlsen    15 年前

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

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

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

    我的猜测是,这两种方法中的一种使代码无法被另一种魔法所识别,并且决定不再花时间在C编译器的当前版本上。当然,这可能根本不是一个明智的选择,而且它只是不起作用,因为没有人想过要实现它。

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

        4
  •  1
  •   Sly1024    15 年前

    我会这样做:

    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
  •  -1
  •   Bolpat    6 年前

    也许这只是一个语法限制。在与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中看到的示例是专门为lambda迭代器创建的。

    在VB中,有 Iterator 关键字;它没有C对应项。imho,这不是C的一个特征,没有真正的原因。

    因此,如果您真的非常想要匿名迭代器函数,那么当前使用的是VisualBasic或(我还没有检查过)f,如 Part #7 在@thomas levesque的答案中(对于f,请按ctrl+f)。