关于C#:yield break是否等同于从返回IEnumerable 的方法返回Enumerable .Empty


Is yield break equivalent to returning Enumerable<T>.Empty from a method returning IEnumerable<T>

这两种方法在我看来是一样的

1
2
3
4
5
6
7
8
9
public IEnumerable<string> GetNothing()
{
    return Enumerable.Empty<string>();
}

public IEnumerable<string> GetLessThanNothing()
{
    yield break;
}

我已经在测试场景中对每一个进行了分析,但在速度上看不到有意义的差异,但是yield break版本稍微快一点。

有没有理由使用一个而另一个?一个比另一个容易阅读吗?打电话的人有没有行为上的差异?


如果您打算总是返回一个空的可枚举的,那么使用Enumerable.Empty()语法更具声明性。

这里的性能差异几乎肯定不显著。在这里,我将关注性能的可读性,直到一个分析器向您显示这是一个问题。


体内含有yield breakyield returnIEnumerable方法转化为状态机。在这种方法中,你不能将收益回报和传统回报混合在一起。我的意思是,如果您在方法的某个部分中生成某些内容,则不能在另一个部分中返回ICollection。

另一方面,假设您正在通过向集合中添加项,然后返回集合的只读副本来实现返回类型为IEnumerable的方法。如果出于某种原因,您只想返回一个空的集合,那么就不能执行yield break。你所能做的就是把钱还给我。

如果你对这两种方式都进行了分析,并且没有显著的变化,那么你可以忘记它:)


I've profiled each in test scenarios and I don't see a meaningful difference in speed, but the yield break version is slightly faster.

我想您的分析测试不包括程序启动速度。yield构造通过为您生成一个类来工作。当它提供您需要的逻辑时,这个额外的代码是很好的,但如果不是,它只会增加磁盘I/O、工作集大小和JIT时间。

如果在ilspy中打开一个包含测试方法的程序并关闭枚举器反编译,您将发现一个名为d__0的类,其中有十几个成员。其MoveNext方法如下:

1
2
3
4
5
6
7
8
9
bool IEnumerator.MoveNext()
{
    int num = this.<>1__state;
    if (num == 0)
    {
        this.<>1__state = -1;
    }
    return false;
}

EmptyEnumerable的工作方式是通过惰性地创建静态空数组。也许检查是否需要创建数组是在单独的基准测试中,EmptyEnumerableyield break慢的原因,但可能需要大量的迭代来克服启动惩罚,并且任何一种方式都不太可能在整体上被注意到,即使在"千次性能剪纸导致死亡"的情况下。


有趣的是,我今天早上读了这篇文章,几个小时后我被这个例子击中了——当你有更多的代码时发现了不同之处:

1
2
3
4
5
6
7
public static IEnumerable<T> CoalesceEmpty<T>(IEnumerable<T> coll)
{
    if (coll == null)
        return Enumerable.Empty<T>();
    else
         return coll;
}

您不能将第一个返回更改为yield break,因为您还必须更改第二个返回(更改为更长的版本)。


似乎yield breakreturn Enumerable.Empty()所做的至少少一个对象。此外,可能有一些检查表明您将与yield break短路。如果没有其他东西的话,它是您的堆栈所经历的一个更少的函数包装器,虽然不明显,但可以检测到它。

不过,我同意另一个贴出的答案。空是做这件事的"首选"方式。