多次枚举期间调用了生成器方法两次

3
我正在尝试理解以下代码的行为原因:

我正在尝试理解以下代码的行为原因:

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

在主函数中,我获得了一个名为x的新“生成器”(请原谅我使用python术语),然后尝试两次枚举它。 第一次,我取前三个元素并将它们打印出来:我期望程序先打印“Hello world!”,然后打印从0到2的数字。 第二次,我取相同的可枚举对象并跳过3个元素。 我期望程序打印从6到9的数字,而不是首先打印“Hello world!”。
但是,第二次程序会再次打印“Hello world!”,然后从3开始枚举,一直到9。
我不明白:为什么Foo()被调用了两次?
编辑1:不是重复问题。链接的“重复”问题询问编写可枚举对象时的最佳实践,而我在这里遇到了消耗其值的问题。
编辑2:我接受了一个答案,它并不好,但包含了一个让我理解问题的参考链接。 对我来说令人困惑的是foreach循环在枚举IEnumerable时的实际工作方式,因此我将在此解释它以供参考。
IEnumerable是值序列-底层实现无关紧要,只要它是可以按顺序给你值的东西即可。 当您在其中使用foreach循环时,它会创建一个Enumerator对象,这是一个光标,用于跟踪循环到达的位置。
因此,第一次循环创建了第一个枚举器,该枚举器必须启动我的生成器方法并打印第一个字符串。
第二个循环无法拾取上一个枚举器(正如我所认为的那样)。 相反,它实例化了第二个枚举器,该枚举器又重新启动了生成器,从而打印了第二个字符串。
对于任何阅读此内容的Pythonista,请注意,它与Python中生成器的工作方式完全相反(在其中重用可迭代对象不会重新启动它)。

我不同意这是一个重复问题。那个问题询问编写可迭代对象的最佳实践;而我正在尝试理解为什么我的生成器会被重置,即使我并不希望它这样做。 - Michele Ippolito
x 在连续调用之间不保存状态,您始终会得到由 Foo() 生成的完整枚举(包括副作用)。 - Helmut D
@HelmutD,谢谢,那最终是原因。我来自Python,那里是生成器本身维护其状态。 - Michele Ippolito
2个回答

2
在C#中,当您在返回或>的方法或属性中使用yield关键字时,该方法(或属性)将成为惰性序列,它不仅返回列表或类似数组的结构,而且运行该方法直到第一个yield语句,然后暂停,直到从调用代码(您的Main方法)"拉取"下一个元素。
因此,当您编写x = Foo()时,您正在设置x等于每次循环时执行Foo的惰性序列。在Foo的主体中的任何副作用将在此状态机的每次迭代中执行一次。
Skip和Take都以惰性序列作为输入,然后根据需要yield return元素,从而产生新的惰性序列。因此,当您编写y = x.Take(3)时,那里没有迭代,但是当您对y进行foreach时,它将有效地执行Foo直到第三个yield语句。

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

同样地,z = x.Skip(3)将创建第三个惰性序列z,通过将xSkip操作组合而成。这里不会立即迭代任何内容,但是当你对z进行foreach循环时,它将执行Foo的全部内容,但会“抛弃”前3个元素。

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


1
@JamesFaix 解释是错误的,是 foreach 调用迭代,而不是 SkipTake - Oliver
1
你认为 SkipTake 是如何实现的?https://github.com/dotnet/corefx/blob/master/src/System.Linq/src/System/Linq/Skip.cs - JamesFaix
1
使用延迟执行 - 移除 foreach 循环,Foo 将不会被调用。 - Oliver
SkipTake 操作在一个惰性序列上(即 Foo() 的结果),根据需要创建一个新的惰性序列来 yield return 值。因此,当您对这些方法的结果进行 foreach 迭代时,实际上是迭代一系列/管道状态机。 - JamesFaix
2
因此,“当您编写y = x.Take(3)时,Foo的主体被迭代到3个元素,然后状态机被处理”是不正确的。 - Oliver
显示剩余3条评论

0
因为它是 `IEnumerable`。这意味着每次调用时都会调用枚举器(您的方法)。 如果您不希望它被调用两次,那么您应该将其实现: var x = Foo().ToArray();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接