【问题标题】:Iterator issue on yield IEnumerableyield IEnumerable 上的迭代器问题
【发布时间】:2012-08-23 16:09:18
【问题描述】:

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

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 每次枚举时都不会从默认状态开始?有人能解释一下我到底在做什么吗?

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

【问题讨论】:

    标签: c# ienumerable yield-return


    【解决方案1】:

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

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

    【讨论】:

    • 出于某种原因,我希望它可以处理列表的副本,以便它可以保持其初始状态原始状态并且在二次运行时保持不变。我想它没有做任何复杂的事情是有道理的。关于您的示例,我理解那个示例,我只是认为我的示例不同,因为列表位于枚举器内部。也许我需要查看 eh 编译器为该方法实际生成的内容......
    • 基本上是一样的,因为谁从列表中删除项目并不重要,重要的是在开始第二个foreach之前它们已经被删除。如果您不说应该创建新列表,则不会创建新列表,即使它对您的特定场景有用。
    • 是的。我想我现在明白了。我忘记了编程的基础知识,似乎在想一些聪明的事情正在发生。所以基本上创建的迭代器都有一个存储列表的私有字段,但它是对同一个列表的引用,因此删除一个会在后续删除它。
    • 我已经意识到,如果我将 var bang = new List&lt;int&gt;(input); 放在我的可枚举方法的开头,我期望在您的 cmets 和一些从代码的反编译版本开始之后会发生什么这样它总是会在每个循环上存储一个新的东西副本。我想我现在明白为什么它没有了。谢谢您的帮助。 :)
    【解决方案2】:

    您的观察可以用这个较短版本的 main 方法重现:

    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 为空,所以没有什么可以返回。

    【讨论】:

      【解决方案3】:

      嗯,懒惰的评估是什么打击你。您会看到,当您创建 yield return 样式的方法时,它不会在调用时立即执行。但是,它会在您遍历序列后立即执行。

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

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

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

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

      【讨论】:

      • 是的,我认为这是懒惰的评估,我认为这和其他聪明的事情让我看不到更简单的问题,即迭代器对象只有一个列表副本,而我的问题是修改它在方法中列出。
      猜你喜欢
      • 1970-01-01
      • 2012-07-18
      • 1970-01-01
      • 2013-11-28
      • 2011-08-23
      • 2011-02-13
      • 2011-02-15
      相关资源
      最近更新 更多