在foreach

时间:2016-11-15 14:05:03

标签: c# linq foreach iterator

请参阅下面的LinqPad脚本。

在实现工作流时,我从集合(IEnumerable)中获取下一组HasRun任务。迭代Linq查询的结果集时,我将任务更改为HasRun = true。调试显示我最初获得了四个对象的预期子集,但是,在子集全部被标记之后,可枚举突然解析为下一个子集,并且foreach循环也继续在该集合上继续,然后是下一个,等等。 p>

因此,当我希望迭代四次时,它会继续运行,直到迭代了所有三个子集(9个项目)。 这可以通过.ToList()可枚举来轻松解决,但我想知道这是否是故意行为。

在谷歌搜索中,我发现了对迭代变量this old post being one example, on which @jon skeet commented on的“不可预测的行为”的引用,但是最新的c#规范(第8.8.4节)没有提到不可预测的行为,它只是提到了赋值,增量的问题和减量:

  

如果嵌入语句尝试修改迭代变量(通过赋值或++和运算符)或将迭代变量作为ref或out参数传递,则会发生编译时错误。

这种行为是否符合设计要求?

void Main()
{
    List<Foo> foos = new List<UserQuery.Foo>
    {
        new Foo{ SetNbr = 1, HasRun = false },
        new Foo{ SetNbr = 1, HasRun = false },
        new Foo{ SetNbr = 1, HasRun = false },
        new Foo{ SetNbr = 1, HasRun = false },
        new Foo{ SetNbr = 2, HasRun = false },
        new Foo{ SetNbr = 2, HasRun = false },
        new Foo{ SetNbr = 3, HasRun = false },
        new Foo{ SetNbr = 3, HasRun = false },
        new Foo{ SetNbr = 3, HasRun = false }
    };

    //Grab the first subset of Foos where HasRun is false, in order of SetNbr
    var firstNotRunGroup = foos.Where(a => a.SetNbr == (foos.Where(f => f.HasRun == false).Min(f => f.SetNbr)));

    foreach (Foo foo in firstNotRunGroup)
    {
        //foo = new Foo(); < Fails, as expected
        foo.HasRun = true;
        Console.WriteLine(foo.SetNbr);
    }
}

class Foo
{
    public int SetNbr { get; set; }
    public bool HasRun { get; set; }
}

输出:

1 1 1 1 2 2 3 3 3

1 个答案:

答案 0 :(得分:8)

您必须记住LINQ操作返回查询而不是执行查询的结果。每次迭代LINQ序列时,它都会重新计算该查询的结果。这意味着,如果您的查询基于某些基础集合或数据存储(在这种情况下,您正在使用List)并且该数据发生更改,则查询的后续迭代将反映这些更改。

最重要的是,LINQ查询尽可能在迭代期间推迟尽可能多的计算;他们只计算提供下一个值所需的数量。这意味着在枚举期间对基础数据存储的更改可能会影响涉及如何计算查询其余部分的计算。

那么,你的代码做了什么。首先,您声明一个实际上没有做任何事情的查询firstNotRunGroup

然后我们开始在firstNotRunGroup中迭代foreach。它执行谓词,a是列表中的第一项。 a.SetNbr1。然后我们查询foos,查找未运行的项目的最低设置数。那将是1,它是一个匹配,所以返回第一个项目。然后,我们将HasRun设置为true,然后将其打印出来。

现在foreach会检查第二项是否匹配。再次查询foos,并且未运行项目的最低设置数为1,这是第二项的匹配,因此它会运行它。这种情况发生了两次。

现在列表中的前四个项目都已运行,foreach现在将检查是否应返回列表中的第五个项目。 SetNbr2,当它迭代foo以查看未运行项目的最小设置数时,它会看到第一组中的所有项目都已运行,因此2是尚未运行的项目的最小设定数。 2匹配我们查询的项目的设置编号,因此应该运行。

从这两种模式中可以看出,集合中的每个项目都将最终运行。有许多事情可以改变这一点;如果列表没有按照设置编号的升序排列的项目,那么整个事情就会中断(以不同的方式,取决于列表的排序方式),如果你计算了一个项目尚未运行的最小集合一次< / em>,而不是重新计算集合中每个项目的值,这不会发生(并且你的代码也不会如此可怕地扩展),或者如你所说,如果你计算了整个项目集合第一组在开始运行项目之前没有运行,那么你就不会得到这个结果。