c# – 在foreach期间改变属性时解释可枚举的“不可预测的行为”

请参阅下面的LinqPad脚本.

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

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

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

A compile-time error occurs if the embedded statement attempts to modify the iteration variable (via assignment or the ++ and operators) or pass the iteration variable as a ref or out parameter.

这种行为是设计的吗?

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

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

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

那么,你的代码做了什么.首先,您声明一个实际上不执行任何操作的查询firstNotRunGroup.

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

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

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

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

转载注明原文:c# – 在foreach期间改变属性时解释可枚举的“不可预测的行为” - 代码日志