在C#中使用yield return iterators的目的/优点是什么?

我见过的所有使用yield return x;的例子都yield return x; 在一个C#方法内可以通过返回整个列表以相同的方式完成。 在这些情况下,使用yield return语句与返回列表是否有什么好处?

另外,在什么types的情况下会yield return ,你不能只返回完整的列表?

但是如果你自己build立一个系列呢?

一般来说,迭代器可以用来懒惰地生成一系列对象 。 例如Enumerable.Range方法内部没有任何种类的集合。 它只是按需生成下一个数字。 这个使用状态机的惰性序列生成有许多用途。 其中大部分都在function性编程概念之下。

在我看来,如果你正在通过一个集合来查看迭代器(这只是最简单的用例之一),那么你就错了。 正如我所说的,迭代器是返回序列的手段。 序列甚至可能是无限的 。 没有办法返回无限长的列表,并使用前100个项目。 它有时候是懒惰的。 返回一个集合与返回一个集合生成器 (这是一个迭代器) 有很大的不同 。 这是比较苹果和橙子。

假设的例子:

 static IEnumerable<int> GetPrimeNumbers() { for (int num = 2; ; ++num) if (IsPrime(num)) yield return num; } static void Main() { foreach (var i in GetPrimeNumbers()) if (i < 10000) Console.WriteLine(i); else break; } 

此示例打印的质数小于10000.您可以轻松地将其更改为打印less于一百万的数字,而无需触及素数生成algorithm。 在这个例子中,你不能返回所有素数列表,因为序列是无限的,消费者甚至不知道它从一开始就想要多less物品。

这里的好答案表明yield return的好处是你不需要创build一个列表 ; 列表可能是昂贵的。 (另外,过一会儿,你会发现他们体积庞大,不雅。)

但是如果你没有列表呢?

yield return允许您以多种方式遍历数据结构 (不一定是Lists)。 例如,如果您的对象是树,则可以按照前后顺序遍历节点,而无需创build其他列表或更改基础数据结构。

 public IEnumerable<T> InOrder() { foreach (T k in kids) foreach (T n in k.InOrder()) yield return n; yield return (T) this; } public IEnumerable<T> PreOrder() { yield return (T) this; foreach (T k in kids) foreach (T n in k.PreOrder()) yield return n; } 

懒惰评估/延期执行

“yield return”迭代器块将不会执行任何代码,直到您实际调用该特定结果为止。 这意味着他们也可以有效地链接在一起。 stream行测验:假设“ReadLines()”函数读取文本文件中的所有行,并使用迭代器块实现,则下面的代码将迭代多less次?

 var query = ReadLines(@"C:\MyFile.txt") .Where(l => l.Contains("search text") ) .Select(l => int.Parse(l.SubString(5,8)) .Where(i => i > 10 ); int sum=0; foreach (int value in query) { sum += value; } 

答案恰恰是一个,直到在foreach循环中。

关注点分离

再次使用上面假设的ReadLines()函数,我们现在可以很容易地将代码从代码中读取文件的代码,从代码中过滤掉不需要的代码,从实际分析结果的代码中分离出代码。 第一个特别是可重用的。

无限列表

看到我对这个问题的回答就是一个很好的例子:
C#的fibonacci函数返回错误

基本上,我使用一个永远不会停止的迭代器块实现斐波那契序列(至less在达到MaxInt之前),然后以安全的方式使用该实现。

改进的语义

这是用散文来解释比用简单的视觉更难的东西之一:

必要性与功能性分离的关注

如果看不到该图像,则会显示相同代码的两个版本,并为不同的问题提供背景亮点。 linq代码将所有颜色都很好地组合在一起,而传统的命令代码则将颜色混合在一起。 作者认为(我同意)这个结果是使用linq vs使用命令式代码的典型代码… linq能够更好地组织你的代码,使得代码段之间有更好的stream动。


1我相信这是最初的来源: https : //twitter.com/mariofusco/status/571999216039542784 。 还要注意这个代码是Java,但是C#会是类似的。

在玩具/示范场景中,没有太大的区别。 但是在某些情况下,产生迭代器是有用的 – 有时整个列表不可用(例如stream),或者列表计算起来很昂贵并且不可能完全需要。

有时你需要返回的序列太大而不适合内存。 例如,大约3个月前,我参与了MS SLQ数据库之间的数据迁移项目。 数据以XML格式导出。 对于XmlReader来说, 产量回报是相当有用的。 它使编程更容易。 例如,假设一个文件有1000个Customer元素 – 如果你只是把这个文件读入内存,这将需要将它们全部存储在内存中,即使它们是按顺序处理的。 所以,你可以使用迭代器来逐一遍历集合。 在这种情况下,你只需要花费一个元素的内存。

事实certificate,为我们的项目使用XmlReader是使应用程序工作的唯一方法 – 它工作了很长一段时间,但至less它没有挂起整个系统,并没有引发OutOfMemoryExceptionexception 。 当然,你可以在不使用yield迭代器的情况下使用XmlReader 。 但迭代器使我的生活变得更容易了(我不会为了导入而编写代码,所以很快而且没有问题)。 观看这个页面 ,看看如何使用yield iterator来解决实际问题(不仅仅是科学的无限序列)。

如果整个名单是巨大的,它可能会吃掉很多的记忆只是坐在旁边,而与产量你只玩你需要什么,当你需要它,不pipe有多less项目。

看看Eric White的博客(顺便说一下,优秀的博客)关于懒惰与急切评估的讨论 。

使用yield return可以迭代项目,而不必构build列表。 如果你不需要列表,但是想迭代一些项目,可以更容易编写

 foreach (var foo in GetSomeFoos()) { operate on foo } 

 foreach (var foo in AllFoos) { if (some case where we do want to operate on foo) { operate on foo } else if (another case) { operate on foo } } 

你可以把所有的逻辑确定你是否想在你的方法中使用yield return来操作foo,并且你的foreach循环可以更加简洁。

这是我以前接受的答案完全相同的问题:

Yield关键字的价值增加?

查看迭代器方法的另一种方法是,他们做了一个“彻头彻尾”的algorithm。 考虑一个parsing器。 它从stream中提取文本,在其中查找模式并生成内容的高级逻辑描述。

现在,通过采用SAX方法,我可以简化自己作为parsing器作者,在SAX方法中,我有一个callback接口,只要我find下一个模式,就会通知我。 所以在SAX的情况下,每次我find一个元素的开始,我调用beginElement方法,等等。

但是这给我的用户带来了麻烦。 他们必须实现处理接口,所以他们必须编写一个响应callback方法的状态机类。 这很难做到,所以最简单的方法就是使用构buildDOM树的库存实现,然后他们将能够方便地走树。 但是整个结构在内存中被缓冲了 – 不好。

但是怎么样,而不是我写我的parsing器作为一个迭代器方法?

 IEnumerable<LanguageElement> Parse(Stream stream) { // imperative code that pulls from the stream and occasionally // does things like: yield return new BeginStatement("if"); // and so on... } 

这将不会比编写callback接口方法困难 – 只是返回从我的LanguageElement基类派生的对象,而不是调用callback方法。

用户现在可以使用foreach循环我的parsing器的输出,所以他们得到一个非常方便的命令式编程接口。

结果是自定义API的两边看起来像是在控制中 ,因此更容易编写和理解。

使用yield的基本原因是它自己生成/返回一个列表。 我们可以使用返回的列表进一步迭代。