何时不使用屈服值(返回值)

这个问题已经有了答案:
在返回 IEnumable 时,有没有理由不使用屈服返回值

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

我在寻找关于 没有何时使用 yield return的想法。例如,如果我希望返回集合中的所有项,那么像 yield这样的 看起来不是很有用吗?

在哪些情况下,使用 yield将是有限的,不必要的,给我带来麻烦,或其他应该避免?

48869 次浏览

如果你不想让一个代码块返回一个迭代器来将循序存取传递给一个基础集合,那么你就不需要 yield return。你只需 return的集合,然后。

要认识到的关键是 yield有什么用处,然后您可以决定哪些情况不能从中受益。

换句话说,当您不需要延迟计算序列时,您可以跳过 yield的使用。那是什么时候?当您不介意立即将整个集合存储在内存中时,就是这种情况。否则,如果有一个巨大的序列会对内存造成负面影响,那么就需要使用 yield来一步一步地处理它(即懒惰地处理)。在比较这两种方法时,分析器可能会派上用场。

注意大多数 LINQ 语句是如何返回 IEnumerable<T>的。这允许我们不断地将不同的 LINQ 操作串在一起,而不会对每个步骤的性能产生负面影响(也就是延迟执行)。另一种情况是在每个 LINQ 语句之间放置一个 ToList()调用。这将导致在执行下一个(链接的) LINQ 语句之前立即执行前面的每个 LINQ 语句,从而放弃惰性计算的任何好处,直到需要时才使用 IEnumerable<T>

在什么情况下使用屈服 将是有限的,不必要的,得到我 陷入麻烦,否则应该是 逃避?

我能想到几个例子:

  • 在返回现有迭代器时避免使用屈服返回值。例如:

    // 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);
    }
    

当您需要随机访问时,收益率将是有限的/不必要的。如果您需要访问元素0,然后访问元素99,那么基本上就消除了延迟计算的有用性。

如果您定义了一个 Linq-y 扩展方法,其中包装了实际的 Linq 成员,那么这些成员通常会返回一个迭代器。自己通过迭代器屈服是不必要的。

除此之外,使用屈服来定义一个在 JIT 基础上进行评估的“流”可枚举不会遇到太多麻烦。

如果您正在序列化枚举的结果并通过连接发送它们,那么可能会发现一个问题。由于执行被推迟到需要结果时,您将序列化一个空枚举并将其发送回来,而不是发送所需的结果。

在什么情况下使用收益率将是有限的,不必要的,给我带来麻烦,或其他应该避免?

在处理递归定义的结构时,仔细考虑使用“屈服返回值”是一个好主意。例如,我经常看到:

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)。由于二叉树的高度是 lgn < = h < = n,这意味着该算法在时间上最好为 O (nlgn) ,最差为 O (n ^ 2) ,在堆栈空间上最好为 O (lgn) ,最差为 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

我不得不维护一堆代码,这些代码来自一个完全痴迷于收益率返回和 IEnumable 的家伙。问题是,我们使用的许多第三方 API 以及我们自己的许多代码都依赖于 List 或 Array。所以我不得不这样做:

IEnumerable<foo> myFoos = getSomeFoos();
List<foo> fooList = new List<foo>(myFoos);
thirdPartyApi.DoStuffWithArray(fooList.ToArray());

不一定是坏事,但是有点烦人,有时会导致在内存中创建重复的 List,以避免重构所有内容。

Eric Lippert 提出了一个很好的观点(可惜 C # 没有 像水流一样平缓的溪流)。我要补充的是,有时候由于其他原因,枚举过程的开销很大,因此,如果您打算多次迭代 IEnumable,那么应该使用列表。

例如,LINQ- 到对象是建立在“产量返回”的基础上的。如果你已经编写了一个缓慢的 LINQ 查询(例如过滤一个大的列表到一个小的列表,或者做排序和分组) ,它可能是明智的调用 ToList()的查询结果,以避免枚举多次(实际上执行查询多次)。

如果在编写一个方法时,您正在“屈服返回值”和 List<T>之间进行选择,请考虑: 计算每个元素是否都很昂贵,调用者是否需要枚举多次结果?如果你知道答案是肯定的和肯定的,你就不应该使用 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.
}

每当调用者更改某些让步函数假定不更改的内容时,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>();
}

更新

以下是 我的标杆的结果:

Benchmark Results

这些结果显示了执行1,000,000次操作所花费的时间(以毫秒为单位)。

在重新考虑这个问题时,性能差异不足以让人担心,因此您应该使用最容易阅读和维护的方法。

更新2

我敢肯定,上述结果是通过禁用编译器最佳化来实现的。在使用现代编译器的发布模式下运行,两者的性能似乎几乎没有区别。选择你最能读懂的。

如果该方法在调用该方法时具有预期的副作用,我将避免使用 yield return。这是由于 卡塔林老爹提到过的延迟执行。

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

我的经验法则是:

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