在C#中,为什么匿名方法不能包含yield语句?
-
10-07-2019 - |
题
我认为做这样的事情会很好(使用 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。我想知道为什么。这 产量文档 只是说不允许。
由于不允许,我只是创建了列表并将项目添加到其中。
解决方案
Eric Lippert 最近写了一系列关于为什么在某些情况下不允许使用屈服的博客文章。
编辑2:
- 第7部分 (这个是后来发的,专门解决了这个问题)
您可能会在那里找到答案......
编辑1:这在第 5 部分的评论中得到了解释,在 Eric 对 Abhijeet Patel 评论的回答中:
问:
埃里克,
您还能提供一些有关为什么在匿名方法或lambda表达式中不允许“产量”的见解
A :
好问题。我很想拥有匿名迭代器块。能够在本地变量上构建一个就地的小序列发生器,这将是非常棒的。原因不直接的原因:好处不会超过成本。在大多数情况下,在宏伟的方案中,将序列发生器的令人敬畏的生成器的出色表现实际上很小。因此,好处不是那么引人注目。
成本是很大的。迭代器的重写是编译器中最复杂的转换,而匿名方法重写是第二复杂的。匿名方法可以在其他匿名方法中,并且匿名方法可以在迭代器块内部。因此,我们要做的是首先我们重写所有匿名方法,以便它们成为封闭类的方法。这是编译器在发出IL之前的第二层事情。完成该步骤后,迭代器重写器可以假定迭代器块中没有匿名方法。他们都已经重写了。因此,迭代器重写器可以专注于重写迭代器,而不必担心其中可能存在未实现的匿名方法。
另外,与匿名方法不同,迭代器块永远不会“巢”。迭代器重写器可以假定所有迭代器块都是“顶级”。
如果允许匿名方法包含迭代器块,则两个假设都会出现在窗口中。您可以拥有一个包含匿名方法的迭代块,该方法包含一个匿名方法,该方法包含包含匿名方法的迭代器块,并且...恶心。现在,我们必须编写一个重写通行证,该通行证可以同时处理嵌套的迭代器块和嵌套匿名方法,将我们的两个最复杂的算法融合到一个更复杂的算法中。设计,实施和测试真的很难。我敢肯定,我们足够聪明。我们这里有一个聪明的团队。但是,我们不想承担这么巨大的负担,以获得“很高兴拥有但不必要”的功能。——埃里克
其他提示
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();
}
同时迭代器方面正在尝试做一些工作来制作一个小型状态机。某些简单的示例可能需要进行大量的健全性检查(首先处理(可能是任意的)嵌套闭包),然后查看最底层的结果类是否可以转换为迭代器状态机。
然而这将是
- 工作量相当大。
- 如果迭代器块方面至少能够阻止闭包方面应用某些转换以提高效率(例如将局部变量提升为实例变量而不是完全成熟的闭包类),则不可能在所有情况下工作。
- 如果在不可能或非常难以不实施的情况下存在哪怕很小的重叠机会,那么由此产生的支持问题的数量可能会很高,因为许多用户将失去微妙的突破性变化。
- 它可以很容易地解决。
在你的例子中像这样:
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;
}
不幸的是,我不知道为什么他们不允许这样做,因为当然完全可以设想这是如何工作的。
但是,匿名方法已经是<!>“编译器魔术<!>”的一部分了。从某种意义上说,该方法将被提取到现有类中的方法,甚至提取到整个新类,具体取决于它是否处理局部变量。
此外,使用yield
的迭代器方法也是使用编译器魔法实现的。
我的猜测是,这两个中的一个使代码无法识别另一个魔法,并且决定不花时间为当前版本的C#编译器做这项工作。当然,它可能根本不是一个有意义的选择,它只是没有用,因为没有人想过实现它。
对于100%准确的问题,我建议您使用 Microsoft Connect 网站并报告问题,我相信你会得到一些可用的回报。
我会这样做:
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;
干杯,
狡猾
可能只是语法限制。在与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#,如 Part#7 @Thomas Levesque的答案(按Ctrl + F代表F#)。