c# - 说明在 foreach 期间更改属性时可枚举的 "unpredictable behavior"

标签 c# linq foreach iterator

请引用下面的 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 序列时,它都会重新计算当时该查询的结果。这意味着如果您的查询基于某些底层集合或数据存储(在本例中,您使用的是列表)并且数据发生变化,则查询的后续迭代将反射(reflect)这些变化。

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

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

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

现在 foreach 去检查第二项是否匹配。再次查询 foos 并且未运行项目的最低集合编号是 1,这是第二个项目的匹配项,因此它运行它。这种情况又发生了两次。

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

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

关于c# - 说明在 foreach 期间更改属性时可枚举的 "unpredictable behavior",我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40611850/

相关文章:

Javascript 效率 : 'for' vs 'forEach'

c# - 我可以将参数传递给 Xamarin 中的 Clicked 事件吗?

c# - 对父列表中的每个子列表执行 'OrderByDescending'

sql - 使用 Lambda 表达式以及 Join、GroupBy、Count 和 Sum 的 Linq to SQL

javascript - 在循环中使用 Array.prototype.forEach 可以吗?

R:foreach 不适用于导出图形,例如 png 或 ggsave

c# - System.Web.Mvc.ModelState 不包含 'IsValid' 的定义

c# - 在 C# 中使用 Access 数据库?

c# - 如何在 Dynamics CRM 2011 中查询(使用 LINQ)FormattedValues

c# - 将模型导出到数据表