为什么"return"和"yield return"不能在同一个方法中使用?

28

为什么我们不能在同一个方法中同时使用return和yield return呢?

例如,我们可以有下面的GetIntegers1和GetIntegers2方法,但是不能有GetIntegers3方法。

public IEnumerable<int> GetIntegers1()
{
  return new[] { 4, 5, 6 };
}

public IEnumerable<int> GetIntegers2()
{
  yield return 1;
  yield return 2;
  yield return 3;
}

public IEnumerable<int> GetIntegers3()
{
  if ( someCondition )
  {
    return new[] {4, 5, 6}; // compiler error
  }
  else
  {
    yield return 1;
    yield return 2;
    yield return 3;
  }
}

13
等一下,Jon Skeet现在就会来。 - Juvanis
我想补充一点,如果你真的需要的话,你可以创建一个GetIngegers4函数,根据条件调用GetIntegers1或者GetIntegers2。 - xanatos
这可能很明显,但在这种情况下,您始终可以展开集合并yield返回其中的项:foreach(var item in new[]{4,5,6}) yield return item; - Foo42
5个回答

22

return是急切的,一次性返回整个结果集。而yield return则构建了一个枚举器(enumerator)。在使用yield return时,C#编译器会在后台发出必要的类来生成枚举器。编译器不会在确定是否应该发出可枚举对象的代码或者有一个返回简单数组的方法时,查找运行时条件,比如if ( someCondition )。它会检测到你的方法中同时使用了这两种情况,这是不可能的,因为它不能同时发出枚举器的代码和使方法返回正常的数组,尤其是对于同一个方法。


当我尝试调试代码时,我可以看到它迭代遍历了所有的代码行。那么我们为什么可以说它在内部构建了枚举器并仅返回一次呢? - Karan

12

不,你不能这样做 - 迭代器块(使用 yield 的东西)不能使用常规的(非 yield)return。相反,你需要使用两个方法:

public IEnumerable<int> GetIntegers3()
{
  if ( someCondition )
  {
    return new[] {4, 5, 6}; // compiler error
  }
  else
  {
    return GetIntegers3Deferred();
  }
}
private IEnumerable<int> GetIntegers3Deferred()
{
    yield return 1;
    yield return 2;
    yield return 3;
}

或者,由于在这种特定情况下,另外两种方法中已经存在这两个代码:

public IEnumerable<int> GetIntegers3()
{
  return ( someCondition ) ? GetIntegers1() : GetIntegers2();
}

8
编译器会重新编写任何带有yield语句(return或break)的方法。目前它无法处理可能具有或不具有yield的方法。
我建议阅读Jon Skeet的C# in Depth第6章,该章节可免费获取-它很好地涵盖了迭代块。
我认为在未来版本的c#编译器中这是可能的。其他.Net语言支持类似于“yield from”运算符的东西(查看)。如果c#中存在这样的运算符,它将允许您以以下形式编写代码:
public IEnumerable<int> GetIntegers()
{
  if ( someCondition )
  {
    yield! return new[] {4, 5, 6};
  }
  else
  {
    yield return 1;
    yield return 2;
    yield return 3;
  }
}

3
从理论上讲,我认为返回和yield返回可以混合使用:编译器很容易先将任何return (blabla());语句在语法上转换为:
var myEnumerable = blabla();
foreach (var m in myEnumerable) 
    yield return m; 
yield break; 

然后继续(将整个方法转换为......现在他们转换成了什么,内部匿名IEnumerator类?!)

那么为什么他们不选择实现它,这里有两种猜测:

  • 他们可能认为让用户同时使用return和yield return会令人困惑,

  • 返回整个枚举是更快、更便宜的,但也是急切的;通过yield return构建比较昂贵(特别是如果递归调用的话,请参见Eric Lippert在此处关于使用yield return语句遍历二叉树的警告:https://dev59.com/lW865IYBdhLWcg3wIa_M#3970171,例如),但是是慵懒的。因此,用户通常不会想混合使用这些:如果您不需要懒惰(即您知道整个序列),就不要承担效率损失,只需使用普通方法。他们可能希望强制用户沿着这些思路进行思考。

另一方面,似乎确实存在用户可以从某些语法扩展中受益的情况;您可能希望阅读这个问题和答案作为示例(不是同一个问题,但可能具有类似的动机):Yield Return Many?


这实际上是对“微软决定不支持的原因可能是什么?”的回答,这本来是一个更好的问题。 - R. Schreurs

1

我认为它不能正常工作的主要原因是,设计一个既不过于复杂又能够高效运行的方式会很困难,而且收益相对较小。

你的代码到底会做什么?它会直接返回数组,还是会遍历它?

如果它会直接返回数组,那么你就必须考虑在什么条件下允许使用return,因为在yield return之后使用return是没有意义的。而且你可能需要生成复杂的代码来决定,方法是否将返回自定义迭代器或数组。

如果你想遍历集合,你可能需要一些更好的关键字。比如yield foreach。实际上,这个关键字曾经被考虑过,但最终没有实现。我记得主要原因是,如果有几个嵌套的迭代器,要使其性能良好实际上非常困难。


1
嵌套迭代器很难使其性能良好,但这个问题可以通过一些巧妙的方法解决;C-Omega就是这样做的。然而,如果你这样做,当这些嵌套迭代器包含异常处理时,就会遇到正确性问题。这是一个可爱的功能想法,我希望我们能够实现它,但付出与收获的比例太高了。 - Eric Lippert

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