何时不使用收益率(return)
这个问题在这里已经有了答案:
返回一个IEnumerable的时候有没有理由不使用“yield return”?
关于yield return
的好处,这里有几个有用的问题。 例如,
-
有人可以揭开yield关键字的神秘面纱吗?
-
有趣的使用C#收益
关键词 -
什么是yield关键字
我正在寻找什么时候不使用yield return
想法。 例如,如果我期望需要返回集合中的所有项目,那么yield
似乎不会有用,对吧?
有什么情况下使用yield
将是有限的,不必要的,让我陷入困境,否则应该避免?
有什么情况下使用收益将是有限的,不必要的,让我陷入困境,否则应该避免?
在处理recursion定义的结构时,仔细考虑使用“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”将会使O(h)嵌套调用到MoveNext。 由于它对具有n个项目的树执行O(n)次,所以使得algorithmO(hn)。 并且由于二叉树的高度为lg n <= h <= n,这意味着该algorithm最多为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
有什么情况下使用收益将是有限的,不必要的,让我陷入困境,否则应该避免?
我可以想到几个例子,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); }
要实现的关键是yield
是有用的,那么你可以决定哪些案件不从中受益。
换句话说,当你不需要一个序列被懒惰地评估时,你可以跳过yield
的使用。 那是什么时候? 当你不介意把你的全部collections放在记忆里的时候。 否则,如果你有一个巨大的序列,会对记忆产生负面影响,你可能会希望一步步使用yield
(即,懒惰地)。 比较两种方法时,分析器可能会派上用场。
注意大多数LINQ语句如何返回一个IEnumerable<T>
。 这使我们可以不断地将不同的LINQ操作串联起来,而不会对每一步的性能产生负面影响(也就是延迟执行)。 替代图片将在每个LINQ语句之间放置一个ToList()
调用。 这将导致在执行下一个(链接的)LINQ语句之前立即执行每个前面的LINQ语句,从而放弃延迟评估和利用IEnumerable<T>
直到需要的任何好处。
这里有很多优秀的答案。 我会添加这个:不要使用收益率为小的或空的集合,你已经知道的价值:
IEnumerable<UserRight> GetSuperUserRights() { if(SuperUsersAllowed) { yield return UserRight.Add; yield return UserRight.Edit; yield return UserRight.Remove; } }
在这些情况下,创buildEnumerator对象比生成一个数据结构更昂贵,更详细。
IEnumerable<UserRight> GetSuperUserRights() { return SuperUsersAllowed ? new[] {UserRight.Add, UserRight.Edit, UserRight.Remove} : Enumerable.Empty<UserRight>(); }
更新
以下是我的基准testing结果:
这些结果显示了执行1,000,000次操作需要多长时间(以毫秒为单位)。 数字越小越好。
在重新审视这个问题时,性能差异不足以担心,所以你应该用最简单的方法去阅读和维护。
Eric Lippert提出了一个很好的观点(太糟糕了,C#没有像Cw那样stream平 )。 我会补充说,有时枚举过程是昂贵的其他原因,因此,如果你打算迭代IEnumerable不止一次,你应该使用一个列表。
例如,LINQ-to-objectsbuild立在“yield return”上。 如果你已经写了一个慢的LINQ查询(例如,把一个大列表过滤成一个小列表,或者进行sorting和分组),对查询结果调用ToList()
可能是明智的,以避免枚举多个次(实际上多次执行查询)。
如果您在编写方法时在“yield return”和List<T>
之间进行select,请考虑:这是否昂贵,并且调用者是否需要不止一次枚举结果? 如果你知道答案是肯定的,那就不要使用“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. }
如果调用者改变了yielding函数假定不改变的东西, yield return
可能会导致麻烦。
当您需要随机访问时,产量会受到限制/不必要的。 如果你需要访问元素0和元素99,你几乎消除了懒惰评估的有用性。
一个可能会把你赶出去的是如果你正在序列化一个枚举的结果并通过线路发送它们。 由于执行延迟到需要结果时,您将序列化一个空的枚举并将其发回,而不是您想要的结果。
我会避免使用yield return
如果方法有副作用,你期望调用的方法。 这是由于Pop Catalin提到的推迟执行。
一个副作用可能是修改系统,这可能发生在像IEnumerable<Foo> SetAllFoosToCompleteAndGetAllFoos()
这样的方法中,这打破了单一责任原则 。 这是非常明显的(现在…),但不是很明显的副作用可能是设置caching结果或类似的优化。
我的经验法则(再次,现在…)是:
- 只有在返回的对象需要一些处理时才使用
yield
- 如果我需要使用
yield
,在方法中没有副作用 - 如果必须有副作用(并限制caching等),不要使用
yield
,并确保扩大迭代的好处超过成本
我必须维护一堆完全沉迷于产量回报和IEnumerable的人的代码。 问题是我们使用的很多第三方API以及我们自己的很多代码依赖于列表或数组。 所以我最终不得不这样做:
IEnumerable<foo> myFoos = getSomeFoos(); List<foo> fooList = new List<foo>(myFoos); thirdPartyApi.DoStuffWithArray(fooList.ToArray());
不一定是坏的,但有些烦人的处理,有几次它导致在内存中创build重复的列表,以避免重构所有的东西。
当你不想要一个代码块返回一个迭代器来顺序访问一个底层的集合时,你不需要yield return
。 您只需return
集合。
如果你正在定义一个Linq-y扩展方法,那么你正在包装实际的Linq成员,那些成员往往会返回一个迭代器。 通过该迭代器产生自己是没有必要的。
除此之外,使用yield来定义一个在JIT基础上评估的“stream式”可枚举数据库,你不会真正陷入困境。