何时不使用收益率(返回)[重复]

问题描述 投票:153回答:11

这个问题在这里已有答案: Is there ever a reason to not use 'yield return' when returning an IEnumerable?

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

我正在寻找关于何时不使用yield return的想法。例如,如果我希望需要返回集合中的所有项目,那么yield似乎不会有用,对吧?

在什么情况下使用yield会限制,不必要,让我陷入麻烦,或者应该避免?

c# .net yield yield-return
11个回答
143
投票

什么情况下,收益率的使用会受到限制,不必要,让我陷入困境,或者应该避免?

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

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)。你可以这样写:

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)。

进一步阅读:请参阅Wes Dyer关于此主题的文章:

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


2
投票

我必须从一个绝对痴迷于收益率和IEnumerable的人那里维护一堆代码。问题是我们使用的很多第三方API以及我们自己的许多代码依赖于列表或数组。所以我最终不得不做:

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

不一定是坏事,但有点讨厌处理,并且在某些情况下导致在内存中创建重复的列表以避免重构一切。


0
投票

如果您正在定义一个Linq-y扩展方法,其中包含实际的Linq成员,那么这些成员通常会返回迭代器。通过该迭代器自己产生是不必要的。

除此之外,使用yield来定义一个在JIT基础上评估的“流”可枚举,你真的不会遇到太多麻烦。


56
投票

什么情况下,收益率的使用会受到限制,不必要,让我陷入困境,或者应该避免?

我可以想到几个案例,IE:

  • 返回现有迭代器时,请避免使用yield return。例: // 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。例: // 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); }

32
投票

要实现的关键是yield有用,然后你可以决定哪些情况不会从中受益。

换句话说,当您不需要延迟评估序列时,您可以跳过使用yield。那会是什么时候?当你不介意立即把你的整个收藏品留在记忆中时。否则,如果你有一个巨大的序列会对内存产生负面影响,你可能希望使用yield逐步处理它(即懒惰)。在比较两种方法时,分析器可能会派上用场。

注意大多数LINQ语句如何返回IEnumerable<T>。这允许我们不断地将不同的LINQ操作串在一起,而不会在每个步骤(即延迟执行)中对性能产生负面影响。另一张图片是在每个LINQ语句之间调用ToList()。这将导致在执行下一个(链接的)LINQ语句之前立即执行每个前面的LINQ语句,从而放弃延迟评估和利用IEnumerable<T>直到需要的任何好处。


23
投票

这里有很多优秀的答案。我想补充一点:不要在已经知道值的小集合或空集合中使用yield return:

IEnumerable<UserRight> GetSuperUserRights() {
    if(SuperUsersAllowed) {
        yield return UserRight.Add;
        yield return UserRight.Edit;
        yield return UserRight.Remove;
    }
}

在这些情况下,创建Enumerator对象比生成数据结构更昂贵,更冗长。

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

更新

这是my benchmark的结果:

Benchmark Results

这些结果显示执行操作1,000,000次所花费的时间(以毫秒为单位)。数字越小越好。

在重新审视时,性能差异并不足以让人担心,因此您应该选择最容易阅读和维护的内容。

更新2

我很确定上述结果是在禁用编译器优化的情况下实现的。使用现代编译器在发布模式下运行时,两者之间的性能几乎无法区分。选择最易读的内容。


18
投票

Eric Lippert提出了一个好点(太糟糕的C#没有stream flattening like Cw)。我想补充一点,有时枚举过程由于其他原因而很昂贵,因此如果您打算多次迭代IEnumerable,则应该使用列表。

例如,LINQ-to-objects建立在“yield return”之上。如果您编写了一个缓慢的LINQ查询(例如,将大型列表过滤到一个小列表中,或者进行排序和分组),那么在查询结果上调用ToList()以避免多次枚举可能是明智的(实际上多次执行查询)。

如果在编写方法时在“yield return”和List<T>之间进行选择,请考虑:计算每个元素是否都很昂贵,并且调用者是否需要多次枚举结果?如果您知道答案是肯定的,那么您不应该使用yield return(例如,除非产生的List非常大,您无法承受它将使用的内存。请记住,yield的另一个好处是结果list不必一次完全在内存中)。

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

IEnumerable<T> GetMyStuff() {
    foreach (var x in MyCollection)
        if (...)
            yield return (...);
}

如果MyCollection有可能因调用者所做的事情而改变,那么这很危险:

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

每当调用者改变屈服函数假设不改变的东西时,yield return就会引起麻烦。


6
投票

如果方法具有您期望调用方法的副作用,我会避免使用yield return。这是由于Pop Catalin mentions的延期执行。

一个副作用可能是修改系统,这可能发生在像IEnumerable<Foo> SetAllFoosToCompleteAndGetAllFoos()这样打破single responsibility principle的方法中。这很明显(现在......),但不太明显的副作用可能是设置缓存结果或类似优化。

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

  • 如果返回的对象需要一些处理,则仅使用yield
  • 如果我需要使用yield,方法中没有副作用
  • 如果必须有副作用(并限制缓存等),请不要使用yield并确保扩展迭代的好处超过成本

5
投票

当您需要随机访问时,收益率将是限制/不必要的。如果你需要访问元素0然后元素99,你几乎已经消除了延迟评估的有用性。


5
投票

可能会让你感到困惑的是,如果你将枚举的结果序列化并通过网络发送它们。因为执行被推迟到需要结果,所以您将序列化一个空的枚举并将其发送回而不是您想要的结果。


2
投票

当您不希望代码块返回迭代器以便顺序访问底层集合时,您不需要yield return。你只需要return这个系列。

© www.soinside.com 2019 - 2024. All rights reserved.