yield return
的好处,SO上有几个有用的问题,例如:
我想知道什么情况下不应该使用yield return
。例如,如果我需要返回集合中的所有项,似乎yield
并没有用处,对吗?
在什么情况下使用yield
会有限制,不必要,会导致问题或者应该避免使用?yield return
的好处,SO上有几个有用的问题,例如:
我想知道什么情况下不应该使用yield return
。例如,如果我需要返回集合中的所有项,似乎yield
并没有用处,对吗?
在什么情况下使用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;
}
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);
}
}
现在我们仍然使用yield return,但是更加智能。现在我们的时间复杂度为O(n),堆空间复杂度为O(h),栈空间复杂度为O(1)。
更多阅读:请查看Wes Dyer关于此主题的文章:
http://blogs.msdn.com/b/wesdyer/archive/2007/03/23/all-about-iterators.aspx
if(current.Right != null) stack.Push(current.Right); if (current.Left != null) stack.Push(current.Left);
但我仍然不明白您是如何通过添加自己的堆栈来进行优化的。两者仍然都使用yield return,这将以相同的方式运行。能否解释一下? - CME64在哪些情况下使用yield会受到限制,是不必要的,会让我陷入麻烦或者应该避免使用?
我能想到几种情况,例如:
Avoid using yield return when you return an existing iterator. Example:
// 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;
}
Avoid using yield return when you don't want to defer execution code for the method. Example:
// 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);
}
foreach
和yield return
- 例如,当您拥有私有集合时,返回集合本身将允许最终用户对其进行修改(通过适当的强制转换),而第一种方法则不会。 - Grx70.AsReadOnly()
将您的列表返回为 IReadOnlyCollection
。问题解决了。 - ErikE重要的是要认识到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>();
}
这里是我的基准测试结果:
这些结果显示执行 1,000,000 次操作所需的时间(以毫秒为单位)。较小的数字更好。
重新审视后,性能差异并不足以引起担忧,因此您应该选择最容易阅读和维护的方法。
我相当确定上述结果是在禁用编译器优化的情况下实现的。使用现代编译器在发布模式下运行时,两者之间的性能似乎几乎无法区分。选择对您来说最可读的方法即可。
Eric Lippert提出了一个很好的观点(太糟糕了,C#没有像Cw那样的流展开)。我想补充一下,有时枚举过程因其他原因而变得昂贵,因此如果您打算多次迭代IEnumerable,则应使用列表。
例如,LINQ-to-objects是建立在"yield return"之上的。如果您编写了一个缓慢的LINQ查询(例如将大型列表过滤为小型列表或进行排序和分组的查询),则最好调用查询结果上的ToList()
,以避免多次枚举(这实际上会执行多次查询)。
如果您在编写方法时在“yield return”和List<T>
之间进行选择,请考虑:每个单独的元素是否计算昂贵,并且调用者是否需要多次枚举结果?如果您知道答案是肯定的,并且是肯定的,那么就不应该使用yield return
(除非,例如,生成的列表非常大,您无法承受它将使用的内存。请记住,yield
的另一个好处是结果列表不必完全一次性在内存中)。
不使用"yield return"的另一个原因是如果交错操作是危险的。例如,如果您的方法看起来像这样,
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所提到的。IEnumerable<Foo> SetAllFoosToCompleteAndGetAllFoos()
这样的方法中可能会发生这种情况,这违反了单一职责原则。这很明显(现在...),但一个不太明显的副作用可能是将缓存结果或类似的内容设置为优化。yield
yield
,则方法中没有副作用yield
,并确保扩展迭代的好处大于成本IEnumerable<T>
的RemoveAll
方法。如果你使用yield return Remove(key)
,那么如果调用者从未迭代,这些项将永远不会被删除! - Bruce Pierson当您需要随机访问时,yield 操作可能会受到限制或变得不必要。如果您需要访问第 0 个元素和第 99 个元素,则基本上消除了延迟计算的有用性。
enumberable[99]
,我是说如果您只对第99个元素感兴趣,则枚举不是正确的选择。 - Robert Gowland可能会遇到的问题是,如果你正在序列化枚举的结果并将它们发送到网络上。由于执行被延迟到需要结果时,你会序列化一个空的枚举并将其发送回来,而不是你想要的结果。
我需要维护一堆来自一个完全着迷于yield return和IEnumerable的人的代码。问题在于我们使用了很多第三方API,以及我们自己的很多代码都依赖于列表(List)或数组(Array)。所以我最终不得不这样做:
IEnumerable<foo> myFoos = getSomeFoos();
List<foo> fooList = new List<foo>(myFoos);
thirdPartyApi.DoStuffWithArray(fooList.ToArray());
并不一定是坏事,但处理起来有点烦人,在某些情况下,为了避免重新设计所有内容,会导致在内存中创建重复的列表。
myFoos.ToArray()
应该就足够了。 - Ahmad Mageed当你不想让代码块返回一个迭代器以便顺序访问底层集合时,你不需要使用 yield return
。你只需直接返回该集合即可,使用 return
。