在C#中使用yield return迭代器的目的/优势是什么?


80

我见过的yield return x;在C#方法中使用的所有示例都可以通过返回整个列表的相同方式完成。在那种情况下,使用yield return语法与返回列表相比有什么好处或优势?

另外,在哪种类型的方案中yield return,您将无法返回完整列表?


15
您为什么首先假设存在“列表”?如果没有呢?
埃里克·利珀特

2
@Eric,我想这就是我要的。您什么时候没有清单。到目前为止,文件流和无限序列是两个很好的例子。
CoderDennis

1
如果您有此列表,那么可以肯定地将其返回;但是,如果要在方法内部构建列表并返回该列表,则可以/应该使用迭代器。一次生产一个项目。有很多好处。
justin.m.chase

2
自5年前问这个问题以来,我当然学到了很多东西!
CoderDennis

1
使用yields的最大好处是您不必命名另一个中间变量。
nawfal

Answers:


119

但是,如果您自己建立一个收藏集怎么办?

通常,迭代器可用于延迟生成对象序列。例如Enumerable.Rangemethod内部没有任何种类的集合。它只是按需生成下一个数字。使用状态机生成这种惰性序列有很多用途。它们中的大多数都包含在功能编程概念下

在我看来,如果您将迭代器视为枚举集合的一种方式(这只是最简单的用例之一),那么您走错了路。正如我所说的,迭代器是返回序列的手段。序列甚至可能是无限的。无法返回无限长度的列表并使用前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的质数。您可以轻松地将其更改为打印小于一百万的质数,而无需完全接触质数生成算法。在此示例中,您无法返回所有质数的列表,因为该序列是无限的,并且使用者甚至不知道从一开始就想要多少个项目。


对。我已经建立了列表,但是一次返回一个项与返回整个列表有什么区别?
CoderDennis

4
除其他原因外,它使代码更具模块化,因此您可以加载项目,处理然后重复。另外,考虑一种情况,即装载一件物品非常昂贵,或者有很多物品(数以百万计的人)。在那些情况下,不希望加载整个列表。
Dana the Sane

15
@Dennis:对于线性存储在内存中的列表来说,可能没有什么区别,但是例如,如果枚举一个10GB的文件并逐行处理每一行,那将会有所不同。
Mehrdad Afshari,2009年

1
+1是一个很好的答案-我还要补充一点,yield关键字允许将迭代器语义应用于传统上不被认为是集合的源-例如网络套接字,Web服务甚至是并发问题(请参阅stackoverflow.com/questions/ 481714 / ccr-yield-and-vb-net
LBushkin

很好的例子,所以基本上它是一个基于上下文(例如方法调用)的集合生成器,直到有人尝试访问它时才开始采取行动,而没有收益的传统集合方法则需要知道它的大小才能构建并返回完整的集合-然后遍历该集合的所需部分?
Michael Harper

24

这里很好的答案表明,的好处yield return您无需创建列表; 清单可能很昂贵。(此外,过一会儿,您会发现它们笨重而笨拙。)

但是,如果没有列表怎么办?

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

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

1
此示例还突出显示了委派的情况。如果您有一个在某些情况下可以包含其他集合项目的集合,则迭代和使用yield return而不是构建所有结果的完整列表并返回非常简单。
汤姆·梅菲尔德,2009年

1
现在,C#仅需要实现yield!F#的方式,因此您不需要所有foreach语句。
CoderDennis

顺便说一句,您的示例显示了以下方面的“危险”之一yield return:何时会产生有效或无效代码通常并不明显。尽管yield return可以递归使用,但这种用法将在处理深度嵌套的枚举器时增加大量开销。手动状态管理的代码可能更复杂,但运行效率更高。
2012年

16

延迟评估/延迟执行

在您实际要求该特定结果之前,“ yield return”迭代器块不会执行任何代码。这意味着它们也可以有效地链接在一起。弹出测验:以下代码将对该文件进行多少次迭代?

var query = File.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循环中的最后一步。即使我有三个单独的linq运算符函数,我们仍然只能循环遍历文件的内容一次。

除了性能之外,这还有其他好处。例如,我可以编写一个简单而通用的公平方法来一次读取和预过滤日志文件,然后在几个不同的地方使用相同的方法,每次使用都会增加不同的过滤器。因此,我在保持良好性能的同时也有效地重用了代码。

无限列表

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

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

改进语义和关注点分离

再次使用上面的文件示例,我们现在可以轻松地将读取文件的代码与从实际分析结果的代码中过滤掉不需要的行的代码分开。特别是第一个,是非常可重用的。

这是用散文比用简单的视觉1来解释谁更难的事情之一:

势在必行vs功能分离

如果看不到该图像,它将显示同一代码的两个版本,并带有针对不同关注点的背景突出显示。linq代码将所有颜色很好地组合在一起,而传统的命令式代码将颜色混合在一起。作者认为(并且我同意),这种结果是使用linq和使用命令式代码的典型结果……linq可以更好地组织代码,以使各节之间的流程更好。


1我相信这是原始来源:https : //twitter.com/mariofusco/status/571999216039542784。另请注意,此代码是Java,但C#类似于。


1
延迟执行可能是迭代器的最大优点。
justin.m.chase

12

有时,您需要返回的序列太大而无法容纳在内存中。例如,大约3个月前,我参加了一个在MS SLQ数据库之间进行数据迁移的项目。数据以XML格式导出。事实证明,使用XmlReader返回收益非常有用。它使编程变得相当容易。例如,假设一个文件有1000个Customer元素-如果您只是将此文件读入内存,那么即使将它们依次处理,也需要将所有这些元素同时存储在内存中。因此,可以使用迭代器来遍历集合。在这种情况下,您只需要为一个元素花费内存。

事实证明,在项目中使用XmlReader是使应用程序正常工作的唯一方法-它工作了很长时间,但至少它没有挂起整个系统,也没有引发OutOfMemoryException异常。当然,您可以不使用yield迭代器使用XmlReader。但是迭代器使我的工作变得更加轻松(我不会这么快就编写代码来导入代码,而不会遇到麻烦)。观看此页面,以了解如何使用yield迭代器解决实际问题(不仅仅是无限序列的科学问题)。


9

在玩具/演示场景中,没有太多区别。但是在某些情况下,让出迭代器很有用-有时,整个列表不可用(例如流),或者该列表在计算上很昂贵,不太可能需要全部。


2

如果整个列表很大,那么它可能会占用大量内存,只是闲逛而已,而收益率仅在需要时随需要而玩,而不管有多少项目。



2

使用,yield return您可以遍历项目,而无需建立列表。如果您不需要列表,但是想要遍历某些项目集,则编写起来会更容易

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循环更加简洁。


2

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

收益关键字的价值增加了​​吗?

查看迭代器方法的另一种方法是,它们要完成将算法“由内而外”的艰苦工作。考虑一个解析器。它从流中提取文本,在其中寻找模式并生成内容的高级逻辑描述。

现在,我可以采用SAX方法使自己成为解析器作者,这很容易,其中有一个回调接口,每当找到下一个模式时,我都会通知该接口。因此,对于SAX,每次我找到一个元素的开始,就调用该beginElement方法,依此类推。

但这给我的用户带来麻烦。他们必须实现处理程序接口,因此必须编写一个响应回调方法的状态机类。这很难做到,因此最简单的方法是使用构建DOM树的stock实现,然后它们将具有走树的便利。但是,然后整个结构被缓冲在内存中-不好。

但是相反,我将解析器作为迭代器方法编写了呢?

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...
}

这将比回调接口方法更难编写-只需让我们返回从我的LanguageElement基类派生的对象,而不是调用回调方法。

用户现在可以使用foreach遍历解析器的输出,因此他们获得了非常方便的命令式编程界面。

结果是自定义API的两面看上去都在控制之下,因此更易于编写和理解。


2

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


概念上正确,但技术上不正确。它返回一个IEnumerable实例,该实例只是抽象一个迭代器。该迭代器实际上是获取下一项而不是物化列表的逻辑。使用return yield不会生成列表,它只会生成列表中的下一项,并且仅在被要求(重复)时生成。
Sinaesthetic,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.