1

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

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在它为空后立即迭代。

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

我的困惑是为什么会这样?为什么我的 IEnumerable 每次枚举时都不会从默认状态开始?有人可以解释一下我到底在做什么吗?

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

4

3 回答 3

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();,但它给出的结果可以。

于 2012-08-23T16:12:49.050 回答
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
在第一次调用时IterateGetNumbers执行并清空 foo。在第二次调用Iterate,GetNumbers时再次执行,但现在 foo 是空的,所以没有任何东西可以返回。

于 2012-08-23T16:16:08.843 回答
1

好吧,懒惰的评估是打击你的。您会看到,当您创建yield return-style 方法时,它不会在调用时立即执行。但是,一旦您遍历序列,它将立即执行。

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

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

Iterate因此,在您第一次之前,所有列表都已填满。(IEnumerablecreated byGetNumbers持有对它们的引用,因此您覆盖的事实foo并不重要。)在第一个期间,所有三个都被清空Iterate。接下来,下一次迭代以相同的内部状态开始,但改变了外部状态,给出不同的结果。

I'd like to notice, that mutation and depending on outer state is generally frowned upon in functional programming style. The LINQ is actually a step toward functional programming, so it's a good idea to follow the FP's rules. So you could do better with just not removing the items from input in GetNumbers.

于 2012-08-23T16:16:42.673 回答