【问题标题】:Generator method called twice during multiple enumeration在多次枚举期间调用了两次生成器方法
【发布时间】:2017-12-04 11:10:52
【问题描述】:

我试图理解为什么下面的代码会这样:

using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApplication
{
    internal class Program
    {
        private static IEnumerable<int> Foo()
        {
            Console.WriteLine("Hello world!");
            for (var i = 0; i < 10; i++)
                yield return i;
        }

        public static void Main(string[] args)
        {
            var x = Foo();

            var y = x.Take(3);
            foreach (var i in y)
                Console.WriteLine(i);

            var z = x.Skip(3);
            foreach (var i in z)
                Console.WriteLine(i);
        }
    }
}

在 main 中,我得到了一个名为 x 的新“生成器”(请原谅 python 术语),然后我尝试枚举它两次。 我第一次获取前 3 个元素并打印它们:我希望程序打印“Hello world!”后面是从 0 到 2 的数字。 第二次我采用相同的可枚举并跳过另外 3 个元素。 我希望程序打印从 6 到 9 的数字,而不打印“Hello world!”首先。

相反,程序第二次打印“Hello world!”第二次,然后从 3 开始枚举,直到 9。

我不明白:为什么 Foo() 被调用了两次?

EDIT1:不重复。链接的“重复”询问编写枚举时的最佳实践,在这里我无法使用它的值。

EDIT2:我接受了一个老实说不是很好的答案,但包含一个指向让我理解问题的参考的链接。 让我感到困惑的是 foreach 循环在循环 IEnumerable 时实际上是如何工作的,所以我将在这里解释它以供将来参考。

IEnumerable 是值的序列 - 底层实现无关紧要,只要它可以按顺序为您提供值。 当您在其上使用 foreach 循环时,它会创建一个 Enumerator 对象,该对象基本上是一个游标,用于跟踪循环到达的位置。

所以发生的事情是第一个循环创建了第一个枚举器,它又必须启动我的生成器方法并打印第一个字符串。

第二个循环无法拾取前一个枚举器(正如我所相信的那样)。 相反,它实例化了第二个 Enumerator,然后重新启动生成器,从而打印第二个字符串。

对于阅读本文的任何 pythonista,请注意这与生成器在 python 中的工作方式截然相反(重用可迭代对象不会重新启动它)。

【问题讨论】:

  • 我不同意它是重复的。该问题询问编写可迭代对象时的最佳实践;我试图了解为什么我的发电机会被重置,即使我不希望它会这样做。
  • x 不会在后续调用之间保存状态,您始终会获得由 Foo() 生成的完整枚举(包括副作用)。
  • @HelmutD,谢谢,这最终是原因。我来自 python,它是生成器本身维护其状态。

标签: c# ienumerable


【解决方案1】:

在 C# 中,当您在返回 IEnumerableIEnumerable&lt;T&gt; 的方法或属性中使用 yield 关键字时,该方法(或属性)将成为惰性序列,它不仅仅返回一个列表或类似数组结构,它运行该方法直到第一个 yield 语句,然后暂停直到从调用代码(您的 Main 方法)“拉”下一个元素。

因此,当您编写x = Foo() 时,您将x 设置为等于每次循环执行Foo 主体的惰性序列。 Foo 主体中的任何副作用将在此状态机的每次迭代中执行一次。

SkipTake 都将惰性序列作为输入,然后根据需要将yield return 元素作为输入,从而产生一个新的惰性序列。所以当你写y = x.Take(3)时,那里没有任何迭代,但是当你foreachy时,它会有效地执行Foo直到第三个yield语句。

Skip源代码: https://github.com/dotnet/corefx/blob/master/src/System.Linq/src/System/Linq/Skip.cs

同样,z = x.Skip(3) 使 z 成为第三个惰性序列,由 xSkip 操作组合而成。这里没有立即迭代,但是当你foreachz 时,它会执行Foo 的整个主体,但会“丢弃”前3 个元素。

参见 Jon Skeet 的书:http://csharpindepth.com/Articles/Chapter6/IteratorBlockImplementation.aspx

【讨论】:

  • 这对我来说非常清楚......但我的状态机没有被处置。我仍然有一个有效的参考。我不是在做z = Foo().Skip(3),我在做z = x.Skip(3),其中 x 已经被迭代了。
  • @JamesFaix 解释有误,调用迭代的是foreach,而不是SkipTake
  • 延迟执行 - 删除 foreaches 并且 Foo 根本不会被调用
  • 完全正确 - 因此“当您编写 y = x.Take(3) 时,Foo 的主体最多迭代 3 个元素,然后状态机被处理掉”是不正确的
【解决方案2】:

因为它是`IEnumerable'。这意味着每次调用枚举器(您的方法)时都会调用它。 如果您不希望它被调用两次,那么您应该实现它:

var x = Foo().ToArray();

【讨论】:

    猜你喜欢
    • 2020-09-19
    • 2017-10-15
    • 2011-02-09
    • 1970-01-01
    • 1970-01-01
    • 2016-07-19
    • 2023-03-28
    • 2011-09-11
    • 1970-01-01
    相关资源
    最近更新 更多