为什么在lambda中不能使用yield,但可以在lambda中使用await?

26

1
除了迭代器 lambda 之外,还可以使用迭代器表达式。 这个特性看起来像这样:IEnumerable<int> e = enum { yield return 1; };。 这将使参数验证变得更加简单,因为您不需要提取第二个方法。 - usr
2
虽然我很乐意提供一些帮助,但我注意到这不是一个关于实际代码的具体问题,而是一个关于设计和实现团队动机的历史性问题,只有那些团队中的人才能回答。这种问题不适合在StackOverflow上提问。 - Eric Lippert
3个回答

26
据Eric Lippert所述,匿名迭代器没有添加到语言中,因为在现有编译器中实现该复杂功能的成本过高。但实际上,相关成本是实现成本,但它是在一个没有架构设置以实现该复杂功能的现有编译器中产生的成本。
编译器必须为异步方法和迭代器执行相同的操作(将它们转换为状态机),因此我非常困惑为什么不允许使用匿名迭代器,而允许使用匿名异步方法。
简要历史:C# 2.0中首先引入了匿名方法和迭代器块。当我在C# 3.0中添加Lambda表达式时,重构所有现有匿名方法代码以处理所有Lambda的新特性是一项重大成本。这使得它变得更加复杂和昂贵。判断将迭代器块Lambda化为收益太小,成本太高;这将占据总成本的很大一部分。我们无力承担。如果你把Developer Division中每个团队的工作计划相加,那么C# 3.0编译器团队的“最长杆”团队,而我在语义分析器上的工作是编译器团队中的最长杆。每天我们可能会延误C# 3.0发布,那将是Visual Studio落后的一天。因此,任何不能使LINQ更好的东西都被裁减了,包括迭代器Lambda。
在C# 4中,迭代器Lambda是考虑的众多功能之一。我们有一个潜在的良好功能列表,长度比你的手臂还长,我们只能承担不到十分之一的功能。在C# 5中,团队添加了异步方法。 设计和实现团队长时间尝试找到一个共同的抽象概念,既适用于迭代器块又适用于等待重写;正如您所指出的那样,它们显然相似。但是,最终发现找到通用解决方案的成本并不划算。通用性令人惊讶地昂贵,并且如果设计上仅统一两件事情的通用性不便宜,那么这种做法就很傻。
因此,决定将等待重写器实现为其自身的东西。鉴于团队将承担这个巨大的成本,并且考虑到异步方法的原始转换无论如何都要变成lambda形式,决定投资于全功能:包含lambda的异步方法,包含lambda的异步lambda,整个交易。该功能的成本只占整个功能非常昂贵的一小部分。
而且,我们再次遇到了长杆问题。任何可能会破坏await的lambda引擎的工作都应该避免,包括尝试使它们与迭代器块配合工作。
现在比较一下Visual Basic。VB很长一段时间都没有迭代器块。当它们被添加时,不存在任何现有的基础设施来保持工作!整个东西可以从头开始构建,以处理包含lambda和包含迭代器块的迭代器块,因此已经完成了这项工作。
C#编译器已通过Roslyn项目进行了彻底的重新架构和重写。我希望这将降低在假设的将来版本的C#中实现迭代器块lambda的成本。我们拭目以待!

我只知道C#编译器进行的高级转换(迭代块->状态机,异步方法->状态机),所以我认为将其泛化不会很复杂。从你的回答中我了解到,两者之间存在许多细微差别和实现细节,这使得制定一个通用解决方案变得更加困难,这正是我想要的答案。 - jakobbotsch
1
@Janiels:除了适应基础架构来处理两种状态机的难度之外,还有其他方面需要考虑。例如,假设明天团队决定通过对代码进行巧妙更改来克服在 catch 中等待的限制。现在我们有一个问题。在 catch 中使用 yield return 是不合法的。要么他们幸运地做出的更改可以同时在 catch 中启用 yield return 而不会破坏任何东西,要么这种更改将不稳定处理尝试结构内部的 yield 返回的现有代码。如果您有通用解决方案,则后者的风险很高。 - Eric Lippert
2
@Janiels:简而言之,代码重用实际上很少能节省你认为的那么多。通常更好的做法是利用有限的资源制作两个类似的东西,每个都能很好地完成一件事,并且可以随意修改而不影响另一个,而不是制作一个能够充分胜任两种语言编译的编译器。Roslyn团队在这个问题上进行了数月的辩论:我们应该拥有一个能够编译C#和VB的编译器,还是拥有两个编译器,每个都能很好地编译一种语言,并且可以在未来独立地进行更改?我们选择了后者。 - Eric Lippert
@EricLippert,我不明白为什么它不能进行翻译呢?给那些未命名的迭代器随机命名怎么就无法实现了呢? - Pacerier

5
匿名迭代器块虽然很好,但并没有特别强烈的好处。将迭代器块重构为自己的方法并不是一个巨大的障碍。 async匿名方法更具有概念意义,不需要像匿名迭代器块那样进行重构,并且具有更加强烈的终端用户受益。
简而言之,这些好处值得付出实现的代价,不像迭代器块那样。成本可能相当可比。

当你需要将迭代器块重构为新类时,因为你需要在其中捕获一些局部变量,这就开始变得麻烦了。即使如此,这也不是一个大问题 - 我只是想知道为什么编译器对于异步lambda会这样做,但对于lambda中的迭代器却不会,当机制是相同的。 - jakobbotsch
1
为什么C#没有实现X功能?答案总是一样的:因为没有人设计、规定、实现、测试、文档化和发布该功能。这六个步骤都是必要的,才能实现一个功能。所有这些都需要花费大量的时间、精力和金钱。功能不是便宜的,我们非常努力地确保只发布那些在有限的时间、精力和资金预算下给我们的用户带来最大可能好处的功能。 - Erik Philips
@Janiels 通常情况下,您希望在迭代器块中关闭本地变量的时间是可以接受委托,然后让该委托关闭变量的情况。 - Servy
@Erik Philips 是的,但这个问题不仅是“为什么C#没有实现X功能?”的形式,而是“为什么C#没有实现X功能,当它看起来与已经实现的Y功能非常相似?”的形式。正如Eric Lippert所指出的那样,这是因为在底层,它们并不像我预期的那样相似,这种情况下你提供的链接就有意义了;我完全同意并接受这一点。 - jakobbotsch

1

看看这段代码(它并不起作用,只是一个例子):

Task<IEnumerable<int>> resultTask = new Task<IEnumerable<int>>(() =>
{
    for (int i = 0; i < 10; ++i)
    {
        yield return i;
    }
});

你不觉得它有点没有结构吗?

假设使用整个lambda范围,处理yield "惰性"会非常困难且不值得。

然而,有很好的方法从并行任务中返回 yield

但让我们看看以下事情。定义一个带有yield返回的方法:

static IEnumerable<int> GetIntegers()
{
    for (int i = 0; i < 10; ++i)
    {
        yield return i;
    }
}

把它放在 lambda 中将会起作用:
Task<IEnumerable<int>> resultTask = new Task<IEnumerable<int>>(() =>
{
    return GetIntegers();
});

这段代码的行为方式是什么?它会失去真正的yield优势吗?

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