关于yield IEnumerable的迭代器问题

时间:2012-08-23 16:09:19

标签: c# ienumerable yield-return

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

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每次枚举时都不会从默认状态启动?有人可以解释我到底在做什么吗?

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

3 个答案:

答案 0 :(得分:6)

你的迭代器确实从它的初始状态开始。但是,它会修改它正在读取的列表,一旦列表被清除,你的迭代器就没有任何东西要做了。基本上,考虑

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();更改,但会给出结果。

答案 1 :(得分:3)

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

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);GetNumbers之间加注Iterate来验证这一点 在第一次调用Iterate时,执行GetNumbers并清空foo。 在第二次调用Iterate时,再次执行GetNumbers,但现在foo为空,因此没有任何内容可以返回。

答案 2 :(得分:1)

嗯,懒惰的评价是什么打击你。您会看到,当您创建yield return样式方法时,它不会在调用时立即执行。然而,只要你重复序列就会执行它。

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

问题在于,您的IEnumersble不仅依赖于内部状态,还依赖于外部状态。该外部状态是foo列表的内容。

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

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