代码之家  ›  专栏  ›  技术社区  ›  Lawrence P. Kelley

何时不使用收益率(收益率)【副本】

  •  153
  • Lawrence P. Kelley  · 技术社区  · 14 年前
    < Buff行情>

    此问题已在此处找到答案:
    返回IEnumerable时是否有理由不使用“yield-return”?

    < /块引用>

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

    我正在寻找有关何时使用 yield return的想法。例如,如果我希望需要返回集合中的所有项,则它不会 seep. like. yield. will be used,right?

    这里有几个关于 yield return . 例如,

    我在想什么时候 不是 使用 生利 . 例如,如果我希望返回集合中的所有项,则它不会 似乎 喜欢 yield 会有用的,对吗?

    在什么情况下使用 产量 是限制,不必要,让我陷入麻烦,还是应该避免?

    11 回复  |  直到 5 年前
        1
  •  140
  •   Eric Lippert    14 年前

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

    在处理递归定义的结构时,仔细考虑使用“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)。

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

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

        2
  •  56
  •   Wai Ha Lee captain-yossarian from Ukraine    9 年前

    在什么情况下使用收益率 会限制,不必要,抓住我 陷入困境,否则应该 避免?

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

    • 在返回现有迭代器时避免使用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);
      }
      
        3
  •  32
  •   Ahmad Mageed    14 年前

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

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

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

        4
  •  23
  •   StriplingWarrior    6 年前

    这里有很多很好的答案。我要加上一个:对于已经知道值的小集合或空集合,不要使用yield返回:

    IEnumerable<userright>getSuperUserRights()。{
    如果(超级用户已分配){
    收益归还使用权。
    收益率返回用户权利。编辑;
    让渡归还使用权。移除;
    }
    }
    < /代码> 
    
    

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

    IEnumerable<userright>getSuperUserRights()。{
    返回已分配的超级用户
    ?新建[]userright.add,userright.edit,userright.remove
    :可枚举。空<用户权限>();
    }
    < /代码> 
    
    < H2>更新< /H2>
    
    

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

    这些结果显示执行操作1000000次需要多长时间(毫秒)。数字越小越好。

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

    更新2

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

    价值观:

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

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

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

    更新

    以下是my benchmark:

    Benchmark Results

    这些结果显示执行操作1000000次需要多长时间(毫秒)。数字越小越好。

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

    更新2

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

        5
  •  17
  •   Qwertie    5 年前

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

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

    如果你在“收益回报”和 List<T> 在编写方法时,请考虑:每个元素的计算成本是否很高,调用方是否需要多次枚举结果?如果你知道答案是肯定的,你就不应该使用 yield return (例如,除非生成的列表非常大,而且您负担不起它将使用的内存。记住,另一个好处是 yield 结果列表不必同时完全在内存中)。

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

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

    生利 当调用者更改生成函数假定不变的内容时,会导致问题。

        6
  •  6
  •   Community gkalpak    7 年前

    我会避免使用 yield return 如果该方法在调用该方法时具有预期的副作用。这是由于延迟执行 Pop Catalin mentions .

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

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

    • 只使用 yield 如果要返回的对象需要进行一些处理
    • 如果我需要的话,这个方法没有副作用 产量
    • 如果必须有副作用(并限制到缓存等),不要使用 产量 确保扩展迭代的好处大于成本
        7
  •  5
  •   Robert Gowland    14 年前

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

        8
  •  5
  •   Aidan    14 年前

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

        9
  •  2
  •   explorer    14 年前

    如果不希望代码块返回迭代器以顺序访问底层集合,则不需要 yield return . 你简单 return 然后收集。

        10
  •  2
  •   Mike Ruhlin    14 年前

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

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

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

        11
  •  0
  •   KeithS    14 年前

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

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