When NOT to use yield (return)
This question already has an answer here:
Is there ever a reason to not use 'yield return' when returning an IEnumerable?
号
关于
- 百万千克1
有人能揭开产量的神秘面纱吗?关键字
百万千克1百万千克1
C产量的有趣使用关键字
百万千克1百万千克1
yield关键字是什么
百万千克1
型
我在想什么时候不使用
在哪些情况下,使用
What are the cases where use of yield will be limiting, unnecessary, get me into trouble, or otherwise should be avoided?
在处理递归定义的结构时,仔细考虑使用"yield return"是一个好主意。例如,我经常看到:
1 2 3 4 5 6 7 8 9 | 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)。你可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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
What are the cases where use of yield
will be limiting, unnecessary, get me
into trouble, or otherwise should be
avoided?
我可以考虑几个案例,例如:
在返回现有迭代器时避免使用yield return。例子:
1
2
3
4
5
6
7
8
9
10
11
12// 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。例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 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);
}
要认识到的关键是
换言之,当您不需要对序列进行惰性评估时,可以跳过使用
注意大多数LINQ语句如何返回
这里有很多很好的答案。我要补充一点:对于已经知道值的小集合或空集合,不要使用yield-return:
1 2 3 4 5 6 7 | IEnumerable<UserRight> GetSuperUserRights() { if(SuperUsersAllowed) { yield return UserRight.Add; yield return UserRight.Edit; yield return UserRight.Remove; } } |
在这些情况下,创建枚举器对象比生成数据结构更昂贵、更详细。
1 2 3 4 5 | IEnumerable<UserRight> GetSuperUserRights() { return SuperUsersAllowed ? new[] {UserRight.Add, UserRight.Edit, UserRight.Remove} : Enumerable.Empty<UserRight>(); } |
更新
以下是我的基准测试结果:
这些结果显示执行操作1000000次所用的时间(毫秒)。数字越小越好。
在重新审视这一点时,性能差异还不足以令人担忧,因此您应该选择最容易阅读和维护的内容。
更新2我很肯定上面的结果是在禁用编译器优化的情况下得到的。在使用现代编译器的发布模式下运行,性能在这两者之间几乎是不可区分的。选择对你来说最可读的东西。
埃里克·利珀特提出了一个很好的观点(糟糕的是C没有像CW那样的流平坦)。我要补充的是,有时由于其他原因,枚举过程是昂贵的,因此如果您打算多次迭代IEnumerable,那么应该使用一个列表。
例如,linq-to对象建立在"yield-return"之上。如果您编写了一个缓慢的LINQ查询(例如,将一个大列表过滤成一个小列表,或者进行排序和分组),那么最好对查询结果调用
如果在编写方法时在"yield return"和
另一个不使用"收益率返回"的原因是如果交错操作是危险的。例如,如果您的方法看起来像这样,
1 2 3 4 5 | IEnumerable<T> GetMyStuff() { foreach (var x in MyCollection) if (...) yield return (...); } |
如果由于调用方所做的某些事情而导致myCollection发生变化,则这是危险的:
1 2 3 4 5 6 | foreach(T x in GetMyStuff()) { if (...) MyCollection.Add(...); // Oops, now GetMyStuff() will throw an exception // because MyCollection was modified. } |
每当调用者更改生成函数假定不变的内容时,
如果该方法在调用该方法时具有预期的副作用,我将避免使用
其中一个副作用是修改系统,这可能发生在像
我的经验法则(再一次,现在…)是:
- 仅当返回的对象需要一点处理时才使用
yield 。 - 如果我需要使用
yield ,方法中没有副作用。 - 如果必须有副作用(并将其限制在缓存等),不要使用
yield ,并确保扩展迭代的好处大于成本。
一个可能会让您抓到的问题是,如果您正在序列化一个枚举的结果,并通过网络发送它们。因为执行会延迟到需要结果为止,所以您将序列化一个空枚举并将其发送回,而不是发送您想要的结果。
当您需要随机访问时,yield将是限制/不必要的。如果您需要访问元素0,然后访问元素99,那么您已经基本上消除了延迟评估的有用性。
我必须维护一堆代码,这些代码来自一个完全沉迷于收益率返回和IEnumerable的人。问题是,我们使用的许多第三方API以及我们自己的许多代码都依赖于列表或数组。所以我不得不这样做:
1 2 3 | IEnumerable<foo> myFoos = getSomeFoos(); List<foo> fooList = new List<foo>(myFoos); thirdPartyApi.DoStuffWithArray(fooList.ToArray()); |
不一定很糟糕,但处理起来有点烦人,而且在某些情况下,它会导致在内存中创建重复的列表,以避免重构所有内容。
如果不希望代码块返回迭代器以顺序访问底层集合,则不需要
如果要定义一个Linq-Y扩展方法,在该方法中包装实际的Linq成员,那么这些成员通常不会返回迭代器。不需要自己通过迭代器屈服。
除此之外,使用yield定义一个"流式"可枚举(在jit基础上进行评估)并不会遇到太多麻烦。