我写了一个程序,旨在从给定的起点创建一个随机数字列表。这是一个快速而肮脏的东西,但我在玩它时发现了一个有趣的效果,我不太明白。
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);
放在对GetNumbers
和Iterate
的调用之间来验证这一点
在对Iterate
的第一次调用中,执行GetNumbers
并清空foo。在对Iterate
的第二次调用中,再次执行GetNumbers
,但现在foo为空,因此没有任何可返回的内容。
好吧,懒惰的评估会让你大吃一惊。您可以看到,当您创建一个yield return
风格的方法时,它不会在调用时立即执行。然而,一旦您对序列进行迭代,它就会立即执行。
因此,这意味着该列表在GetNumbers
期间不会被清除,而仅在Iterate
期间被清除。实际上,函数GetNumbers
的整个主体将仅在Iterate
期间执行。
您的问题是,您使IEnumersble
不仅依赖于内部状态,还依赖于外部的状态。这种外部状态就是foo
列表的内容。
因此,所有列表都已填充,直到您第一次Iterate
为止。(由GetNumbers
创建的IEnumerable
包含对它们的引用,因此覆盖foo
这一事实并不重要。)在第一个Iterate
期间,所有三个都被清空。接下来,下一次迭代从相同的内部状态开始,但外部状态发生了变化,给出了不同的结果。
我想注意的是,在函数式编程风格中,突变和依赖外部状态通常是不受欢迎的。LINQ实际上是向函数式编程迈出的一步,因此遵循FP的规则是个好主意。因此,在GetNumbers
中不删除input
中的项目可以做得更好。