关于c#:何时不使用收益率(返回)

When NOT to use yield (return)

本问题已经有最佳答案,请猛点这里访问。

This question already has an answer here:
Is there ever a reason to not use 'yield return' when returning an IEnumerable?

关于yield return的好处,这里有几个有用的问题。例如,

    百万千克1

    有人能揭开产量的神秘面纱吗?关键字

    百万千克1百万千克1

    C产量的有趣使用关键字

    百万千克1百万千克1

    yield关键字是什么

    百万千克1

我在想什么时候不使用yield return。例如,如果我希望返回一个集合中的所有项目,那么看起来yield似乎不是很有用,对吗?

在哪些情况下,使用yield会受到限制、不必要、给我带来麻烦,或者应该避免使用其他方法?


What are the cases where use of yield will be limiting, unnecessary, get me into trouble, or otherwise should be avoided?

在处理递归定义的结构时,仔细考虑使用"yield return"是一个好主意。例如,我经常看到:

1
2
3
4
5
6
7
8
9
public static IEnumerable<T> PreorderTraversal<T>(Tree<T> root)
{
    if (root == null) yield break;
    yield return root.Value;
    foreach(T item in PreorderTraversal(root.Left))
        yield return item;
    foreach(T item in PreorderTraversal(root.Right))
        yield return item;
}

看起来非常合理的代码,但它存在性能问题。假设这棵树很深。然后至多会有O(H)嵌套迭代器构建。在外部迭代器上调用"moveNext",然后对moveNext进行o(h)嵌套调用。因为它对一个包含n个项的树执行O(n)次操作,这使得算法成为O(hn)。由于二叉树的高度是lg n<=h<=n,这意味着算法在时间上最好是o(n lg n),最坏是o(n^2),在堆栈空间上最好是o(lg n),最坏是o(n)。它在堆空间中是O(H),因为每个枚举器都分配在堆上。(在C的实现中,我知道;一致的实现可能具有其他堆栈或堆空间特性。)

但是迭代树的时间可以是O(n),堆栈空间可以是O(1)。你可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static IEnumerable<T> PreorderTraversal<T>(Tree<T> root)
{
    var stack = new Stack<Tree<T>>();
    stack.Push(root);
    while (stack.Count != 0)
    {
        var current = stack.Pop();
        if (current == null) continue;
        yield return current.Value;
        stack.Push(current.Left);
        stack.Push(current.Right);
    }
}

它仍然使用收益率回报,但更聪明。现在我们在时间上是O(n),在堆空间上是O(h),在堆栈空间上是O(1)。

进一步阅读:参见韦斯·戴尔关于这一主题的文章:

http://blogs.msdn.com/b/wesdyer/archive/2007/03/23/all-about-iterators.aspx


What are the cases where use of yield
will be limiting, unnecessary, get me
into trouble, or otherwise should be
avoided?

我可以考虑几个案例,例如:

  • 在返回现有迭代器时避免使用yield return。例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // Don't do this, it creates overhead for no reason
    // (a new state machine needs to be generated)
    public IEnumerable<string> GetKeys()
    {
        foreach(string key in _someDictionary.Keys)
            yield return key;
    }
    // DO this
    public IEnumerable<string> GetKeys()
    {
        return _someDictionary.Keys;
    }
  • 如果不想推迟方法的执行代码,请避免使用yield return。例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Don't do this, the exception won't get thrown until the iterator is
    // iterated, which can be very far away from this method invocation
    public IEnumerable<string> Foo(Bar baz)
    {
        if (baz == null)
            throw new ArgumentNullException();
         yield ...
    }
    // DO this
    public IEnumerable<string> Foo(Bar baz)
    {
        if (baz == null)
            throw new ArgumentNullException();
         return new BazIterator(baz);
    }


要认识到的关键是yield对什么有用,然后您可以决定哪些情况不从中受益。

换言之,当您不需要对序列进行惰性评估时,可以跳过使用yield。那是什么时候?当你不介意马上把你的全部收藏放在记忆中的时候。否则,如果您有一个巨大的序列会对内存产生负面影响,那么您将希望使用yield一步一步地(即懒惰地)处理它。当比较这两种方法时,分析器可能会派上用场。

注意大多数LINQ语句如何返回IEnumerable。这允许我们连续地将不同的LINQ操作串在一起,而不会对每个步骤的性能产生负面影响(又称延迟执行)。另一种情况是在每个LINQ语句之间插入一个ToList()调用。这将导致在执行下一个(链接的)LINQ语句之前立即执行前面的每个LINQ语句,从而放弃延迟评估的任何好处,并在需要时使用IEnumerable


这里有很多很好的答案。我要补充一点:对于已经知道值的小集合或空集合,不要使用yield-return:

1
2
3
4
5
6
7
IEnumerable<UserRight> GetSuperUserRights() {
    if(SuperUsersAllowed) {
        yield return UserRight.Add;
        yield return UserRight.Edit;
        yield return UserRight.Remove;
    }
}

在这些情况下,创建枚举器对象比生成数据结构更昂贵、更详细。

1
2
3
4
5
IEnumerable<UserRight> GetSuperUserRights() {
    return SuperUsersAllowed
           ? new[] {UserRight.Add, UserRight.Edit, UserRight.Remove}
           : Enumerable.Empty<UserRight>();
}

更新

以下是我的基准测试结果:

Benchmark Results

这些结果显示执行操作1000000次所用的时间(毫秒)。数字越小越好。

在重新审视这一点时,性能差异还不足以令人担忧,因此您应该选择最容易阅读和维护的内容。

更新2

我很肯定上面的结果是在禁用编译器优化的情况下得到的。在使用现代编译器的发布模式下运行,性能在这两者之间几乎是不可区分的。选择对你来说最可读的东西。


埃里克·利珀特提出了一个很好的观点(糟糕的是C没有像CW那样的流平坦)。我要补充的是,有时由于其他原因,枚举过程是昂贵的,因此如果您打算多次迭代IEnumerable,那么应该使用一个列表。

例如,linq-to对象建立在"yield-return"之上。如果您编写了一个缓慢的LINQ查询(例如,将一个大列表过滤成一个小列表,或者进行排序和分组),那么最好对查询结果调用ToList(),以避免多次枚举(实际上多次执行查询)。

如果在编写方法时在"yield return"和List之间进行选择,请考虑:每个元素的计算成本是否很高,调用方是否需要多次枚举结果?如果你知道答案是肯定的和肯定的,你就不应该使用yield return(除非,例如,所产生的列表非常大,你负担不起它将使用的内存。记住,yield的另一个好处是结果列表不必一次完全在内存中)。

另一个不使用"收益率返回"的原因是如果交错操作是危险的。例如,如果您的方法看起来像这样,

1
2
3
4
5
IEnumerable<T> GetMyStuff() {
    foreach (var x in MyCollection)
        if (...)
            yield return (...);
}

如果由于调用方所做的某些事情而导致myCollection发生变化,则这是危险的:

1
2
3
4
5
6
foreach(T x in GetMyStuff()) {
    if (...)
        MyCollection.Add(...);
        // Oops, now GetMyStuff() will throw an exception
        // because MyCollection was modified.
}

每当调用者更改生成函数假定不变的内容时,yield return都会引起问题。


如果该方法在调用该方法时具有预期的副作用,我将避免使用yield return。这是因为pop catalin提到的延迟执行。

其中一个副作用是修改系统,这可能发生在像IEnumerable SetAllFoosToCompleteAndGetAllFoos()这样的方法中,这打破了单一责任原则。这很明显(现在…),但一个不太明显的副作用可能是设置一个缓存结果或类似于优化。

我的经验法则(再一次,现在…)是:

  • 仅当返回的对象需要一点处理时才使用yield
  • 如果我需要使用yield,方法中没有副作用。
  • 如果必须有副作用(并将其限制在缓存等),不要使用yield,并确保扩展迭代的好处大于成本。


一个可能会让您抓到的问题是,如果您正在序列化一个枚举的结果,并通过网络发送它们。因为执行会延迟到需要结果为止,所以您将序列化一个空枚举并将其发送回,而不是发送您想要的结果。


当您需要随机访问时,yield将是限制/不必要的。如果您需要访问元素0,然后访问元素99,那么您已经基本上消除了延迟评估的有用性。


我必须维护一堆代码,这些代码来自一个完全沉迷于收益率返回和IEnumerable的人。问题是,我们使用的许多第三方API以及我们自己的许多代码都依赖于列表或数组。所以我不得不这样做:

1
2
3
IEnumerable<foo> myFoos = getSomeFoos();
List<foo> fooList = new List<foo>(myFoos);
thirdPartyApi.DoStuffWithArray(fooList.ToArray());

不一定很糟糕,但处理起来有点烦人,而且在某些情况下,它会导致在内存中创建重复的列表,以避免重构所有内容。


如果不希望代码块返回迭代器以顺序访问底层集合,则不需要yield return。那么你只需简单地把收藏品


如果要定义一个Linq-Y扩展方法,在该方法中包装实际的Linq成员,那么这些成员通常不会返回迭代器。不需要自己通过迭代器屈服。

除此之外,使用yield定义一个"流式"可枚举(在jit基础上进行评估)并不会遇到太多麻烦。