这个问题在这里已有答案: Is there ever a reason to not use 'yield return' when returning an IEnumerable?
关于yield return
的好处,这里有几个有用的问题。例如,
我正在寻找关于何时不使用yield return
的想法。例如,如果我希望需要返回集合中的所有项目,那么yield
似乎不会有用,对吧?
在什么情况下使用yield
会限制,不必要,让我陷入麻烦,或者应该避免?
什么情况下,收益率的使用会受到限制,不必要,让我陷入困境,或者应该避免?
在处理递归定义的结构时,仔细考虑使用“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
我必须从一个绝对痴迷于收益率和IEnumerable的人那里维护一堆代码。问题是我们使用的很多第三方API以及我们自己的许多代码依赖于列表或数组。所以我最终不得不做:
IEnumerable<foo> myFoos = getSomeFoos();
List<foo> fooList = new List<foo>(myFoos);
thirdPartyApi.DoStuffWithArray(fooList.ToArray());
不一定是坏事,但有点讨厌处理,并且在某些情况下导致在内存中创建重复的列表以避免重构一切。
如果您正在定义一个Linq-y扩展方法,其中包含实际的Linq成员,那么这些成员通常会返回迭代器。通过该迭代器自己产生是不必要的。
除此之外,使用yield来定义一个在JIT基础上评估的“流”可枚举,你真的不会遇到太多麻烦。
什么情况下,收益率的使用会受到限制,不必要,让我陷入困境,或者应该避免?
我可以想到几个案例,IE:
// 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;
}
// 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
。那会是什么时候?当你不介意立即把你的整个收藏品留在记忆中时。否则,如果你有一个巨大的序列会对内存产生负面影响,你可能希望使用yield
逐步处理它(即懒惰)。在比较两种方法时,分析器可能会派上用场。
注意大多数LINQ语句如何返回IEnumerable<T>
。这允许我们不断地将不同的LINQ操作串在一起,而不会在每个步骤(即延迟执行)中对性能产生负面影响。另一张图片是在每个LINQ语句之间调用ToList()
。这将导致在执行下一个(链接的)LINQ语句之前立即执行每个前面的LINQ语句,从而放弃延迟评估和利用IEnumerable<T>
直到需要的任何好处。
这里有很多优秀的答案。我想补充一点:不要在已经知道值的小集合或空集合中使用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次所花费的时间(以毫秒为单位)。数字越小越好。
在重新审视时,性能差异并不足以让人担心,因此您应该选择最容易阅读和维护的内容。
我很确定上述结果是在禁用编译器优化的情况下实现的。使用现代编译器在发布模式下运行时,两者之间的性能几乎无法区分。选择最易读的内容。
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
就会引起麻烦。
如果方法具有您期望调用方法的副作用,我会避免使用yield return
。这是由于Pop Catalin mentions的延期执行。
一个副作用可能是修改系统,这可能发生在像IEnumerable<Foo> SetAllFoosToCompleteAndGetAllFoos()
这样打破single responsibility principle的方法中。这很明显(现在......),但不太明显的副作用可能是设置缓存结果或类似优化。
我的经验法则(再次,现在......)是:
yield
yield
,方法中没有副作用yield
并确保扩展迭代的好处超过成本当您需要随机访问时,收益率将是限制/不必要的。如果你需要访问元素0然后元素99,你几乎已经消除了延迟评估的有用性。
可能会让你感到困惑的是,如果你将枚举的结果序列化并通过网络发送它们。因为执行被推迟到需要结果,所以您将序列化一个空的枚举并将其发送回而不是您想要的结果。
当您不希望代码块返回迭代器以便顺序访问底层集合时,您不需要yield return
。你只需要return
这个系列。