关于c#:何时不使用收益率(返回)

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?

号

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

    百万千克1

    有人能揭开产量的神秘面纱吗?关键字

    百万千克1百万千克1

    C产量的有趣使用关键字

    百万千克1百万千克1

    yield关键字是什么

    百万千克1

型

我在想什么时候不使用yield return。例如,如果我希望返回一个集合中的所有项目,那么看起来yield似乎不是很有用,对吗?

在哪些情况下,使用yield会受到限制、不必要、给我带来麻烦,或者应该避免使用其他方法?


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


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

换言之,当您不需要对序列进行惰性评估时,可以跳过使用yield。那是什么时候?当你不介意马上把你的全部收藏放在记忆中的时候。否则,如果您有一个巨大的序列会对内存产生负面影响,那么您将希望使用yield一步一步地(即懒惰地)处理它。当比较这两种方法时,分析器可能会派上用场。

注意大多数LINQ语句如何返回IEnumerable。这允许我们连续地将不同的LINQ操作串在一起,而不会对每个步骤的性能产生负面影响(又称延迟执行)。另一种情况是在每个LINQ语句之间插入一个ToList()调用。这将导致在执行下一个(链接的)LINQ语句之前立即执行前面的每个LINQ语句,从而放弃延迟评估的任何好处,并在需要时使用IEnumerable。


这里有很多很好的答案。我要补充一点:对于已经知道值的小集合或空集合,不要使用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>();
}

更新

以下是我的基准测试结果:

Benchmark Results

这些结果显示执行操作1000000次所用的时间(毫秒)。数字越小越好。

在重新审视这一点时,性能差异还不足以令人担忧,因此您应该选择最容易阅读和维护的内容。

更新2

我很肯定上面的结果是在禁用编译器优化的情况下得到的。在使用现代编译器的发布模式下运行,性能在这两者之间几乎是不可区分的。选择对你来说最可读的东西。


埃里克·利珀特提出了一个很好的观点(糟糕的是C没有像CW那样的流平坦)。我要补充的是,有时由于其他原因,枚举过程是昂贵的,因此如果您打算多次迭代IEnumerable,那么应该使用一个列表。

例如,linq-to对象建立在"yield-return"之上。如果您编写了一个缓慢的LINQ查询(例如,将一个大列表过滤成一个小列表,或者进行排序和分组),那么最好对查询结果调用ToList(),以避免多次枚举(实际上多次执行查询)。

如果在编写方法时在"yield return"和List之间进行选择,请考虑:每个元素的计算成本是否很高,调用方是否需要多次枚举结果?如果你知道答案是肯定的和肯定的,你就不应该使用yield return(除非,例如,所产生的列表非常大,你负担不起它将使用的内存。记住,yield的另一个好处是结果列表不必一次完全在内存中)。

另一个不使用"收益率返回"的原因是如果交错操作是危险的。例如,如果您的方法看起来像这样,

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 return都会引起问题。


如果该方法在调用该方法时具有预期的副作用,我将避免使用yield return。这是因为pop catalin提到的延迟执行。

其中一个副作用是修改系统,这可能发生在像IEnumerable SetAllFoosToCompleteAndGetAllFoos()这样的方法中,这打破了单一责任原则。这很明显(现在…),但一个不太明显的副作用可能是设置一个缓存结果或类似于优化。

我的经验法则(再一次,现在…)是:

  • 仅当返回的对象需要一点处理时才使用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());

不一定很糟糕,但处理起来有点烦人,而且在某些情况下,它会导致在内存中创建重复的列表,以避免重构所有内容。


如果不希望代码块返回迭代器以顺序访问底层集合,则不需要yield return。那么你只需简单地把收藏品


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

除此之外,使用yield定义一个"流式"可枚举(在jit基础上进行评估)并不会遇到太多麻烦。