使用yield如何节省时间或内存

本文关键字:内存 节省时间 yield 使用 | 更新日期: 2023-09-27 18:28:31

我是C#的新手,在我尝试学习的以前的语言中没有见过yield的等价物,除了可读性之外,我不相信它有帮助。没有它我活了这么多年,为什么我需要它?

正如我在下面所说,您可以使用yield return逐个吐出T类型的值,而不是将这些值收集到IEnumerable<T>中,然后在最后吐出整个集合。有什么意义?毕竟,我确信在中断函数的执行以复制出单个值时会有一些开销。也许我会运行一些性能测试,看看它在时间方面是否更有效。除此之外,我想知道你是否可以向我展示一个特定的情况,在这种情况下,我需要迭代一个函数收集的一组值,并且只能用yield来完成,或者最好用yield来完成。

使用yield如何节省时间或内存

作为迭代器使用的一个典型例子,考虑一个数字序列迭代器:

IEnumerable<int> fibo() {
  int cur = 0, next = 1;
  while(true) {
    yield return cur;
    next += cur;
    cur = next - cur;
  }
}

现在我们可以选择如何处理该系列,并且只计算所需的元素:

var fibs = fibo();
var sumOfFirst10Fibs = fibs.Take(10).Sum();

另一种有用的模式是使复杂的数据结构变平,如树1:

public class Tree<T> {
    public Tree<T> Left, Right;
    public T value;
    public IEnumerable<T> InOrder() {
      if(Left != null) {
        foreach(T val in Left.InOrder())
          yield return val;
      }
      yield return value;
      if(Right != null) {
        foreach(T val in Right.InOrder())
          yield return val;
      }
    }
  }
}

1正如Alexey在评论中所指出的,按顺序遍历是低效的(尤其是当遍历高大的树时)

其想法是动态生成值您的值集合可能是无限的,或者生成每个值的成本可能很高当您通过IEnumerable调用foreach时,实际上是在调用IEnumerator上的方法,这些方法可以用您喜欢的任何方式实现。使用yield的函数会自动重新实现为仅在请求值时生成值的IEnumerator。当您也想要动态生成值时,您还必须对IEnumerator的实现进行编码,就像yielding函数被替换一样。

在某些特定情况下,使用生成器可能比创建和返回集合更可取:

  • 逐行搜索一个很大的文件。你不想把几GB的文本加载到内存中,所以读一行并yield return是有意义的。当然,你可以写一个循环,但通过将逻辑提取到生成器中,你可以很容易地用数据库表或不同格式的文件替换文件
  • 在树上散步。您可以使用访问者来遍历树,也可以使用生成器以正确的顺序生成节点序列,这两种方法是彼此相反的。注意:递归生成器在C#中是个坏主意
  • 为测试目的生成无限数据,其中每个连续的元素都使用以前的元素来生成自己("在圣诞节的1298456天,我的真爱送给我…"是一个微不足道的例子,你不需要存储1298455天的礼物,只需要存储以前的礼物和当天的列表)

基本上,在每种情况下,如果您不必担心将IEnumerable处理为ICollection,即您将其视为一个值流,而不是具有Count的有限值包,则可以通过使用生成器来节省时间或内存。

在您想要返回的Collection尚未准备好的情况下,收益率可能很有用。也就是说,你在迭代的同时构建列表。通过使用yield return,您实际上只需要在返回之前拥有下一个项目。如果IEnumerable表示一个无限集,则收益率回报率更可取。考虑素数列表,或者一个无限的随机数列表。您永远不能一次返回完整的IEnumerable,所以您可以使用yield return递增地返回列表。

MSDN涵盖了很多内容:

当您在语句中使用yield关键字时,您指示它出现的方法、运算符或get访问器是迭代器。使用yield定义迭代器消除了对显式迭代器的需要extra类(保存枚举状态的类,请参阅IEnumerator(OfT) 例如)和自定义集合类型的IEnumerator模式。

技术实施

以下代码返回一个IEnumerable<迭代器中的string>方法,然后遍历其元素。

IEnumerable<string> elements = MyIteratorMethod();
foreach (string element in elements)
{
   …
}

对MyIteratorMethod的调用不执行该方法的主体。相反,该调用返回IEnumerable<string>插入元素变量
在foreach循环的迭代中,MoveNext方法是需要元素。此调用执行MyIteratorMethod的主体直到到达下一个yield return语句。表达式yield return语句返回的值不仅决定循环体消耗的元素变量,以及元素的当前属性,它是IEnumerable<string>
在每个foreach循环的后续迭代迭代器主体从它停止的地方继续,当它达到收益回报语句。foreach循环在迭代器方法或yield break语句结束。