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

时间:2009-07-06 18:11:22

标签: c# iterator yield-return

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

此外,在yield return使用哪种类型的场景时,您不能只返回完整列表?

10 个答案:

答案 0 :(得分:113)

但是如果你自己建造一个系列怎么办?

通常,迭代器可用于延迟生成一系列对象。例如,Enumerable.Range方法内部没有任何类型的集合。它只会生成下一个按需的数字。使用状态机生成这种延迟序列有很多用途。其中大多数都包含在函数式编程概念中。

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

答案 1 :(得分:24)

这里的好答案表明yield return的好处是 您不需要创建列表 ;列表可能很昂贵。 (此外,过了一会儿,你会发现它们笨重而且不够优雅。)

但是如果你没有List呢?

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

答案 2 :(得分:15)

延迟评估/延期执行

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

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 function returning errors

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

改进的语义

对于那些用简单的视觉 1

的人来说,用散文来解释这些事情要难得多。

Imperative vs Functional Separation of Concerns

如果您看不到图像,则会显示相同代码的两个版本,其中包含针对不同问题的背景突出显示。 linq代码具有很好地分组的所有颜色,而传统的命令式代码具有混合的颜色。作者争辩说(并且我同意)这个结果是使用linq和使用命令式代码的典型结果...... linq在组织代码方面做得更好,以便在各部分之间获得更好的流程。


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

答案 3 :(得分:10)

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

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

答案 4 :(得分:9)

在玩具/演示场景中,没有太大区别。但有些情况下,产生迭代器是有用的 - 有时,整个列表不可用(例如流),或者列表计算成本高,并且不太可能完全需要。

答案 5 :(得分:2)

如果整个列表都很庞大,那么它可能会占用大量的内存而只是坐下来,而产量只会在您需要的时候播放,而不管有多少项目。

答案 6 :(得分:2)

lazy versus eager evaluation上看看Eric White的博客(顺便说一下,优秀的博客)上的讨论。

答案 7 :(得分: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返回所有逻辑来确定是否要在方法内部对foo进行操作,并且foreach循环可以更加简洁。

答案 8 :(得分:2)

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

Yield keyword value added?

查看迭代器方法的另一种方法是,他们努力将算法“从里到外”。考虑一个解析器。它从流中提取文本,在其中查找模式并生成内容的高级逻辑描述。

现在,我可以通过采用SAX方法让我自己成为解析器作者,我有一个回调接口,每当我找到下一个模式时我就会通知它。所以在SAX的情况下,每当我找到元素的开头时,我都会调用beginElement方法,依此类推。

但这给我的用户带来了麻烦。他们必须实现处理程序接口,因此他们必须编写响应回调方法的状态机类。这很难做到,所以最简单的方法是使用构建DOM树的库存实现,然后他们将能够方便地遍历树。但随后整个结构被缓存在内存中 - 并不好。

但是我怎么把我的解析器写成迭代器方法呢?

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 的两面看起来像是在控制,因此更容易编写和理解。

答案 9 :(得分:2)

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