何时不使用收益率(返回)

Law*_*ley 158 .net c# yield yield-return

这个问题在这里已经有了答案:
在返回IEnumerable时是否有理由不使用'yield return'?

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

我正在寻找关于何时使用的想法yield return.例如,如果我预计需要返回一个集合中的所有项目,它没有看起来就像yield是有用的,对不对?

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

Eri*_*ert 144

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

在处理递归定义的结构时,仔细考虑使用"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;
}
Run Code Online (Sandbox Code Playgroud)

完全合理的代码,但它有性能问题.假设树很深.然后最多会有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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 我一直希望听到下一版C#中的'yield foreach` ... (9认同)

Pop*_*lin 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;
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 当您不希望延迟该方法的执行代码时,请避免使用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);
    }
    
    Run Code Online (Sandbox Code Playgroud)

  • 延迟执行的+1 =代码抛出时的延迟异常. (22认同)

Ahm*_*eed 32

要意识到的关键是什么yield是有用的,然后你可以决定哪些情况不会从中受益.

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

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


Str*_*ior 23

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

IEnumerable<UserRight> GetSuperUserRights() {
    if(SuperUsersAllowed) {
        yield return UserRight.Add;
        yield return UserRight.Edit;
        yield return UserRight.Remove;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

更新

这是我的基准测试的结果:

基准测试结果

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

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

更新2

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


Qwe*_*tie 18

埃里克·利珀特(Eric Lippert)提出了一个很好的观点(太糟糕了,C#没有像Cw那样的流扁平化).我想补充一点,有时枚举过程由于其他原因而很昂贵,因此如果您打算多次迭代IEnumerable,则应该使用列表.

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

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

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

IEnumerable<T> GetMyStuff() {
    foreach (var x in MyCollection)
        if (...)
            yield return (...);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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


Reb*_*ott 6

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

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

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

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

  • 这应该是“何时不使用”的第一答案。考虑一个返回 `IEnumerable&lt;T&gt;` 的 `RemoveAll` 方法。如果你使用`yield return Remove(key)`,那么如果调用者从不迭代,项目将永远不会被删除! (2认同)

Rob*_*and 5

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

  • 与收益毫无关系.它是IEnumerator固有的,无论是否使用迭代器块实现. (3认同)
  • 当你需要随机访问时,IEnumerable无法帮助你.你如何访问IEnumerable的元素0或99?猜猜我没看到你想说的话 (2认同)

Aid*_*dan 5

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