yield IEnumerable上的迭代器问题



我写了一个程序,旨在从给定的起点创建一个随机数字列表。这是一个快速而肮脏的东西,但我在玩它时发现了一个有趣的效果,我不太明白。

void Main()
{
    List<int> foo = new List<int>(){1,2,3};
    IEnumerable<int> bar = GetNumbers(foo);
    for (int i = 1; i < 3; i++)
    {
        foo = new List<int>(){1,2,3};
        var wibble = GetNumbers(foo);
        bar = bar.Concat(wibble);
    }
    Iterate(bar);
    Iterate(bar);
}
public void Iterate(IEnumerable<int> numbers)
{
    Console.WriteLine("iterating");
    foreach(int number in numbers)
    {
        Console.WriteLine(number);
    }
}
public IEnumerable<int> GetNumbers(List<int> input)
{
    //This function originally did more but this is a cutdown version for testing.
    while (input.Count>0)
    {
        int returnvalue = input[0];
        input.Remove(input[0]);
        yield return returnvalue;
    }
}

运行的输出是:

iterating
1
2
3
1
2
3
1
2
3
iterating

也就是说,我在bar为空之后立即对其进行第二次迭代。

我认为这与我第一次迭代时清空用于生成列表的列表有关,随后它使用这些现在为空的列表进行迭代。

我的困惑是为什么会发生这种事?为什么每次枚举IEnumerables时,它们都不从默认状态开始?有人能解释一下我到底在这里做什么吗?

需要明确的是,我知道我可以通过在对GetNumbers()的调用中添加.ToList()来解决这个问题,这将强制立即评估和存储结果。

迭代器确实从其初始状态开始。然而,它会修改正在读取的列表,一旦列表被清除,迭代器就没有任何事情可做了

var list = new List<int> { 1, 2, 3 };
var enumerable = list.Where(i => i != 2);
foreach (var item in enumerable)
    Console.WriteLine(item);
list.Clear();
foreach (var item in enumerable)
    Console.WriteLine(item);

enumerable不会被list.Clear();改变,但它给出的结果确实改变了。

您的观察结果可以用主方法的较短版本重现:

void Main() 
{ 
    List<int> foo = new List<int>(){1,2,3}; 
    IEnumerable<int> bar = GetNumbers(foo); 
    Console.WriteLine(foo.Count); // prints 3
    Iterate(bar); 
    Console.WriteLine(foo.Count); // prints 0
    Iterate(bar); 
} 

发生的情况如下:

当您调用GetNumbers时,它实际上并没有被执行。只有当您对结果进行迭代时,才会执行它。您可以通过将Console.WriteLine(foo.Count);放在对GetNumbersIterate的调用之间来验证这一点
在对Iterate的第一次调用中,执行GetNumbers并清空foo。在对Iterate的第二次调用中,再次执行GetNumbers,但现在foo为空,因此没有任何可返回的内容。

好吧,懒惰的评估会让你大吃一惊。您可以看到,当您创建一个yield return风格的方法时,它不会在调用时立即执行。然而,一旦您对序列进行迭代,它就会立即执行。

因此,这意味着该列表在GetNumbers期间不会被清除,而仅在Iterate期间被清除。实际上,函数GetNumbers的整个主体将仅在Iterate期间执行。

您的问题是,您使IEnumersble不仅依赖于内部状态,还依赖于外部的状态。这种外部状态就是foo列表的内容。

因此,所有列表都已填充,直到您第一次Iterate为止。(由GetNumbers创建的IEnumerable包含对它们的引用,因此覆盖foo这一事实并不重要。)在第一个Iterate期间,所有三个都被清空。接下来,下一次迭代从相同的内部状态开始,但外部状态发生了变化,给出了不同的结果。

我想注意的是,在函数式编程风格中,突变和依赖外部状态通常是不受欢迎的。LINQ实际上是向函数式编程迈出的一步,因此遵循FP的规则是个好主意。因此,在GetNumbers中不删除input中的项目可以做得更好。

最新更新