解释“不可预测的行为”;在foreach期间更改属性时可枚举的

本文关键字:属性 枚举 不可预测 解释 foreach | 更新日期: 2023-09-27 18:17:56

参考下面的LinqPad脚本。

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

因此,当我期望迭代四次时,它会继续进行,直到所有三个子集(9项)都被迭代。这很容易通过.ToList()计数来解决,但我想知道这是否是故意的行为。

在谷歌中,我发现引用了迭代变量的"不可预测行为",这篇旧文章就是一个例子,@jon skeet评论过,但最新的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; }
}
输出:

1111223.3.3

解释“不可预测的行为”;在foreach期间更改属性时可枚举的

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

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

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

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

现在foreach检查第二项是否匹配。再次查询foos,没有运行的项目的最低集合数是1,这与第二个项目匹配,因此运行它。这种情况还会发生两次。

现在列表中的前四个项目都已经运行了,foreach现在将检查列表中的第五个项目是否应该返回。SetNbr2,当它遍历foo以查看未运行项目的最小集合数是多少时,它会看到第一个集合中的所有项目都已运行,因此2是尚未运行的项目的最小集合数。2匹配我们正在查询的项目的集合编号,因此应该运行它。

从这两个模式可以看出,集合中的每个项目最终都将运行。有很多事情可以改变这一点;如果列表中没有设置数量的物品按升序整个休息(以不同的方式,这取决于是有序列表),如果你计算最小的设置项没有一旦运行,而不是再计算值设置中的每一项不会发生规模(和你的代码也不是那么可怕),或者像你说的,如果你的整个项目集计算在第一组没有运行前你开始跑步项目,那么你就不会得到这个结果