【问题标题】:When NOT to use yield (return) [duplicate]何时不使用产量(返回)[重复]
【发布时间】:2011-04-27 13:33:39
【问题描述】:

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

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

我正在寻找关于何时使用yield return 的想法。例如,如果我预计需要返回集合中的所有项目,它 似乎yield 一样有用,对吧?

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

【问题讨论】:

  • +1 好问题!
  • 做错事的方式有很多种,这只是一种想象练习。我将您的问题改写为:收益回报的常见不当用法有哪些?
  • 程序员需要像在任何其他领域一样发挥想象力。
  • 此问题被标记为重复,但未提供指向重复问题的链接...是否应该取消重复?
  • 这是一个重要的问题,有有趣和有用的答案,应该重新打开它。

标签: c# .net yield yield-return


【解决方案1】:

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

在处理递归定义的结构时,最好仔细考虑您对“收益回报”的使用。比如我经常看到这样的:

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;
}

看起来非常合理的代码,但它存在性能问题。假设树的深度为 h。然后最多会构建 O(h) 嵌套迭代器。然后在外部迭代器上调用“MoveNext”将对 MoveNext 进行 O(h) 嵌套调用。由于它对具有 n 项的树执行此操作 O(n) 次,因此算法为 O(hn)。并且由于二叉树的高度是 lg n

但是迭代一棵树在时间上可能是 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

【讨论】:

  • 关于第一个算法:你说它是堆空间中的 O(1)。在堆空间中不应该是 O(h) 吗? (随着时间的推移,分配对象的 O(n))
  • 我一直希望在下一版 C# 中听到 yield foreach...
  • Stephen Toub 有一篇文章 (blogs.msdn.com/b/toub/archive/2004/10/29/249858.aspx) 讨论了这个特定示例,以及一个使用两种迭代方法来演示性能差异的汉诺塔谜题求解器。
  • @EricLippert 我建议您在推送之前添加一个条件来检查空值以避免空叶遍历if(current.Right != null) stack.Push(current.Right); if (current.Left != null) stack.Push(current.Left); 但我仍然看不到您如何通过在其中添加自己的堆栈来优化它.两者都仍在使用收益回报,它将以相同的方式运行。你能解释一下吗?
  • @CME64:不要使用完整的二叉树,而是尝试我发布的第一个算法和第二个算法,使用具有 100 个节点的二叉树,其中每个右侧节点都为空,即最大不平衡二叉树树。您会发现在第一个算法中,收益返回被调用了 数千次,而在第二个算法中,则被调用了 数百次。你明白这是为什么吗?
【解决方案2】:

使用yield的情况有哪些 将是限制性的,不必要的,得到我 陷入困境,否则应该 避免?

我能想到几个案例,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);
    }
    

【讨论】:

  • +1 表示延迟执行 = 如果代码抛出则延迟异常。
  • 虽然您通常是对的,但我不同意没有理由将 foreachyield return 一起使用 - 例如当您拥有私有集合时,返回集合本身将允许最终用户修改它(使用适当的转换),而第一种方法则不会。
  • @Grx70 所以用.AsReadOnly() 作为IReadOnlyCollection 返回您的列表。问题解决了。
【解决方案3】:

要了解的关键是yield 的用途,然后您可以决定哪些情况不会从中受益。

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

请注意大多数 LINQ 语句如何返回 IEnumerable&lt;T&gt;。这使我们能够不断地将不同的 LINQ 操作串在一起,而不会对每一步的性能产生负面影响(也称为延迟执行)。另一种情况是在每个 LINQ 语句之间放置一个 ToList() 调用。这将导致在执行下一个(链接的)LINQ 语句之前立即执行每个前面的 LINQ 语句,从而放弃惰性求值的任何好处并在需要时使用IEnumerable&lt;T&gt;

【讨论】:

    【解决方案4】:

    这里有很多很好的答案。我要添加一个:不要对您已经知道值的小型或空集合使用 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的结果:

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

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

    更新 2

    我很确定上述结果是在禁用编译器优化的情况下实现的。使用现代编译器在发布模式下运行,看起来两者的性能几乎没有区别。选择对你来说最易读的东西。

    【讨论】:

    • 这真的慢吗?我会想象构建数组会很慢,如果不是更慢的话。
    • @PRMan:是的,我知道你会怎么想。我用基准更新了我的答案以显示差异。我不知道我最初的测试是否没有正确完成,或者自从我第一次回答这个问题以来 .NET 框架是否提高了性能,但性能差异并没有我记忆中的那么大——当然还不够大在大多数情况下担心。
    • 似乎在测试中使用属性而不是常量会产生不同的结果(双关语)。至少在发布模式下,调用和迭代基于产量结果的方法更快。
    • @Yaurthek:你能提供一个代码示例来说明你的意思吗?我是 seeing similar results 和以前一样从返回属性访问:未优化时收益返回要慢得多,而在发布模式下则稍慢。
    • @StriplingWarrior 我怀疑你的实现已经被优化掉了。 Try this 处于释放模式。 (我增加了迭代次数,因为否则我无法获得稳定的结果)
    【解决方案5】:

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

    例如,LINQ-to-objects 是建立在“收益回报”之上的。如果您编写了一个慢速 LINQ 查询(例如,将一个大列表过滤成一个小列表,或者进行排序和分组),最好在查询结果上调用 ToList() 以避免枚举多个次(实际上多次执行查询)。

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

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

    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】:

    当您需要随机访问时,产量将受到限制/不必要。如果您需要访问元素 0 和元素 99,那么您几乎消除了惰性求值的用处。

    【讨论】:

    • 当你需要随机访问时,IEnumerable 帮不了你。您将如何访问 IEnumerable 的元素 0 或 99?我猜我看不出你想说什么
    • @qstarin,没错!访问元素 99 的唯一方法是遍历元素 0-98,因此除非您只需要 20 亿中的第 99 项,否则惰性求值对您一无所获。我不是说你可以访问enumberable[99] 我是说如果你只对第 99 个元素感兴趣,那么 enumerable 不是要走的路。
    • 这与产量毫无关系。它是 IEnumerator 所固有的,无论它是否使用迭代器块实现。
    • @qstarin,它确实有 something 与产量有关,因为产量将产生一个枚举器。 OP询问何时避免yield,yield导致枚举器,使用枚举器进行随机访问是笨拙的,因此在需要随机访问时使用yield是一个坏主意。他本可以以不同的方式生成可枚举的事实并不能否定使用 yield 不好的事实。你可以用枪打人,也可以用球棒打人……你可以用球棒杀死一个人这一事实并不能否定你不应该射杀他。
    • @qstarin,但是,您指出还有其他方法可以生成 IEnumerator。
    【解决方案7】:

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

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

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

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

    【讨论】:

    • 这应该是“何时不使用”的第一答案。考虑一个返回IEnumerable&lt;T&gt;RemoveAll 方法。如果你使用yield return Remove(key),那么如果调用者从不迭代,那么项目将永远不会被删除!
    • 这是一个很好的主要原因,也很容易记住。您还可以考虑潜在抛出的异常也是副作用。他们也将被推迟。这个,以及你已经有一个可枚举的情况,比如一个键的集合。然后只需返回集合。 :) 懒惰的评估不会给你任何东西。
    【解决方案8】:

    如果您正在序列化枚举的结果并通过网络发送它们,可能会引起您的注意。因为执行被推迟到需要结果时,您将序列化一个空枚举并将其发送回,而不是您想要的结果。

    【讨论】:

      【解决方案9】:

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

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

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

      【讨论】:

      • myFoos.ToArray() 就足够了。
      • "myFoos.ToArray() 应该足够了" ...如果您使用的是 .NET 3.5 或更高版本。
      • 对你们俩都有好处。习惯了老办法。我们现在对大多数东西使用 3.5。
      【解决方案10】:

      如果您不希望代码块返回迭代器以顺序访问底层集合,则不需要yield return。您只需 return 收藏即可。

      【讨论】:

      • 考虑在只读包装器中返回它。调用者可能会将其转换回原始集合类型并对其进行修改。
      【解决方案11】:

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

      除此之外,使用 yield 定义一个基于 JIT 评估的“流式”枚举真的不会有太大的麻烦。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-09-25
        • 2018-07-06
        • 2013-06-27
        • 2019-03-17
        • 2019-12-18
        • 2013-10-25
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多