【问题标题】:What is the purpose/advantage of using yield return iterators in C#?在 C# 中使用 yield return 迭代器的目的/优势是什么?
【发布时间】:2010-11-08 11:24:22
【问题描述】:

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

另外,yield return 会在哪些类型的场景中使用,而您不能只返回完整列表?

【问题讨论】:

  • 你为什么首先假设有“一个列表”?如果没有怎么办?
  • @Eric,我想这就是我要问的。你什么时候不会有一个列表。到目前为止,文件流和无限序列是答案中的两个很好的例子。
  • 如果你有这个列表,那么,当然,只要返回它;但是如果您在方法内部构建一个列表并返回它,那么您可以/应该使用迭代器。一次产出一件物品。有很多好处。
  • 自从 5 年前提出这个问题以来,我确实学到了很多东西!
  • 拥有yields 的最大优势是您不必再命名另一个中间变量。

标签: c# iterator yield-return


【解决方案1】:

但是如果你自己建立一个集合呢?

一般来说,迭代器可用于懒惰地生成对象序列。例如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 的素数。您可以轻松地将其更改为打印小于一百万的数字,而无需触及素数生成算法。在此示例中,您无法返回所有素数的列表,因为序列是无限的,而且消费者甚至不知道从一开始就想要多少个项目。

【讨论】:

  • 对。我已经建立了列表,但是一次返回一项与返回整个列表有什么区别?
  • 除其他原因外,它使您的代码更加模块化,因此您可以加载项目,处理,然后重复。另外,考虑加载一个项目非常昂贵的情况,或者有很多(数百万说)。在这些情况下,不希望加载整个列表。
  • @Dennis:对于内存中线性存储的列表,它可能没有区别,但如果你枚举一个 10GB 的文件并逐行处理,它会有所不同.
  • +1 是一个很好的答案——我还要补充一点,yield 关键字允许将迭代器语义应用于传统上不被视为集合的源——例如网络套接字、Web 服务,甚至并发问题(见stackoverflow.com/questions/481714/ccr-yield-and-vb-net
  • 很好的例子,所以基本上它是一个基于上下文(例如方法调用)的集合生成器,并且在尝试访问它之前不会开始行动,而传统的集合方法没有yield需要知道它的大小来构建和返回一个完整的集合 - 然后迭代该集合的所需部分?
【解决方案2】:

这里的好答案表明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;
}

【讨论】:

  • 这个例子也强调了委托的情况。如果您的集合在某些情况下可能包含其他集合的项目,那么迭代和使用 yield return 非常简单,而不是构建所有结果的完整列表并返回它。
  • 现在 C# 只需要像 F# 那样实现 yield!,这样您就不需要所有的 foreach 语句了。
  • 顺便说一句,您的示例显示了yield return 的“危险”之一:它何时会产生高效或低效的代码通常并不明显。尽管yield return 可以递归使用,但这样的使用会给深度嵌套的枚举器的处理带来很大的开销。手动状态管理的代码可能更复杂,但运行效率更高。
【解决方案3】:

延迟评估/延迟执行

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

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

改进的语义和关注点分离

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

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

如果您看不到图像,它会显示相同代码的两个版本,并带有针对不同问题的背景突出显示。 linq 代码将所有颜色很好地分组,而传统的命令式代码将颜色混合在一起。作者认为(我同意)这个结果是使用 linq 与使用命令式代码的典型结果...... linq 在组织代码方面做得更好,以便在部分之间有更好的流动。


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

【讨论】:

  • 延迟执行可能是迭代器最大的好处。
【解决方案4】:

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

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

【讨论】:

    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

      如果整个列表非常庞大,它可能只是坐在那里就吃掉很多内存,而在收益情况下,您只会在需要时使用您需要的东西,而不管有多少项目。

      【讨论】:

        【解决方案7】:

        lazy versus eager evaluation 上查看 Eric White 博客上的讨论(顺便说一句非常棒的博客)。

        【讨论】:

        • 此链接已失效
        【解决方案8】:

        使用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
            }
        }
        

        您可以将所有用于确定是否要在您的方法中操作 foo 的逻辑使用 yield 返回,并且您的 foreach 循环可以更加简洁。

        【讨论】:

          【解决方案9】:

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

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

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

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

          结果是自定义 API 的双方看起来都在控制之中,因此更容易编写和理解。

          【讨论】:

            【解决方案10】:

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

            【讨论】:

            • 概念上正确但技术上不正确。它返回一个 IEnumerable 的实例,它只是抽象了一个迭代器。该迭代器实际上是获取下一个项目的逻辑,而不是具体化列表。使用return yield 不会生成列表,它只会生成列表中的下一项并且仅在被要求(迭代)时生成。
            猜你喜欢
            • 2011-09-08
            • 2011-01-25
            • 1970-01-01
            • 2022-01-21
            • 2021-10-16
            • 2010-10-12
            • 2017-01-09
            • 2022-01-15
            • 1970-01-01
            相关资源
            最近更新 更多