C# 为什么我能够在迭代LINQ列表时编辑它?

C# 为什么我能够在迭代LINQ列表时编辑它?,c#,linq,C#,Linq,我最近遇到了一个问题,我能够更改我在foreach循环中迭代的IEnumerable对象。我的理解是,在C#中,您不应该能够编辑正在迭代的列表,但在经历了一些挫折之后,我发现这正是发生的事情。我基本上循环了一个LINQ查询,并使用对象ID在数据库中对这些对象进行更改,这些更改影响了.Where()语句中的值 有人对此有解释吗?每次迭代LINQ查询时,它似乎都会重新运行 注意:修复方法是在.Where()之后添加.ToList(),但我的问题是为什么会发生这个问题,也就是说,它是一个bug还是我不

我最近遇到了一个问题,我能够更改我在
foreach
循环中迭代的
IEnumerable
对象。我的理解是,在C#中,您不应该能够编辑正在迭代的列表,但在经历了一些挫折之后,我发现这正是发生的事情。我基本上循环了一个LINQ查询,并使用对象ID在数据库中对这些对象进行更改,这些更改影响了
.Where()
语句中的值

有人对此有解释吗?每次迭代LINQ查询时,它似乎都会重新运行

注意:修复方法是在
.Where()
之后添加
.ToList()
,但我的问题是为什么会发生这个问题,也就是说,它是一个bug还是我不知道的东西

using System;
using System.Linq;

namespace MyTest {
    class Program {
        static void Main () {
            var aArray = new string[] {
                "a", "a", "a", "a"
            };
            var i = 3;
            var linqObj = aArray.Where(x => x == "a");
            foreach (var item in linqObj ) {
                aArray[i] = "b";
                i--;
            }
            foreach (var arrItem in aArray) {
                Console.WriteLine(arrItem); //Why does this only print out 2 a's and 2 b's, rather than 4 b's?
            }
            Console.ReadKey();
        }
    }
}
这段代码只是一个可复制的模型,但我希望它能循环4次,并将
aArray
中的所有字符串更改为b。但是,它只循环两次,并将
aArray
中的最后两个字符串转换为b

编辑:在一些反馈之后,更简洁地说,我的主要问题是:“为什么我能够在循环的过程中改变我循环的内容?”。看起来压倒性的答案是LINQ执行延迟执行,所以当我在LINQ IEnumerable中循环时,它正在重新计算


编辑2:实际上,仔细看,似乎每个人都关心
.Count()
函数,认为这就是问题所在。但是,您可以注释掉这一行,我仍然有LINQ对象更改的问题。我更新了代码以反映主要问题

对第一个问题的解释,为什么每次迭代
LINQ查询时都会重新运行它是因为
LINQ

此行仅声明linq EXRPESION,不执行它:

var linqLIST = aArray.Where(x => x == "a");
这就是它被执行的地方:

foreach (var arrItem in aArray)

explict调用
ToList()
将立即运行
Linq
表达式。像这样使用它:

var linqList = aArray.Where(x => x == "a").ToList();
{ "a", "a", "a", "b" }
关于编辑的问题:

当然,
Linq
表达式在每个foreach迭代中都会被评估。问题不在于
Count()
,而是对LINQ表达式的每次调用都会重新计算它。如上所述,将其枚举到
列表中
,然后在列表中迭代

后期编辑:

关于Eric Lippert的批评,我也将参考并详细讨论OP的其他问题

//为什么只打印2个a和2个b,而不是4个b

在第一次循环迭代中,
i=3
,因此在
aArray[3]=“b”您的数组将如下所示:

var linqList = aArray.Where(x => x == "a").ToList();
{ "a", "a", "a", "b" }
在第二个循环迭代中,
i
(-)现在具有值2,并且在执行
aArray[i]=“b”之后您的阵列将是:

{ "a", "a", "b", "b" }
此时,数组中仍有
a
,但
LINQ
查询返回
IEnumerator.MoveNext()==false
,因此循环达到退出条件,因为
IEnumerator
在内部使用,现在到达数组索引中的第三个位置,当重新计算
LINQ
时,它不再与where
x==“a”
条件匹配

为什么我能在循环的过程中改变我循环的内容

之所以能够这样做,是因为
visualstudio
中的内置代码分析器没有检测到您修改了循环中的集合。在运行时修改数组,更改
LINQ
查询的结果,但在数组迭代器的实现中没有处理,因此不会引发异常。 这种缺失的处理似乎是出于设计考虑,因为数组的大小是固定的,只适用于在运行时抛出此类异常的列表

考虑以下示例代码,这些代码应与初始代码示例等效(编辑前):

此代码将编译并迭代循环一次,然后抛出带有以下消息的
System.InvalidOperationException

Collection was modified; enumeration operation may not execute.
现在,
List
实现在枚举时抛出此错误的原因是,它遵循了一个基本概念:
For
Foreach
迭代控制流语句,在运行时需要确定性。此外,
Foreach
语句是的
C#
特定实现,它定义了一个算法,该算法意味着顺序遍历,因此在执行过程中不会改变。因此,
列表
实现在枚举集合时修改集合时引发异常

您找到了在迭代循环并在每次迭代中重新开发循环时修改循环的方法之一。这是一个糟糕的设计选择,因为如果
LINQ
表达式不断更改结果并且从未满足循环的退出条件,则可能会遇到无限循环。这将使调试变得困难,并且在阅读代码时不明显

相反,有
while
控制流语句,它是一个条件构造,在运行时是非确定性的,具有一个特定的退出条件,在执行时预期会发生变化。 根据您的例子考虑重写:

using System;
using System.Linq;

namespace MyTest {
    class Program {
        static void Main () {
            var aArray = new string[] {
                "a", "a", "a", "a"
            };
            bool arrayHasACondition(string x) => x == "a";
            while (aArray.Any(arrayHasACondition))
            {
                var index = Array.FindIndex(aArray, arrayHasACondition);
                aArray[index] = "b";
            }
            foreach (var arrItem in aArray)
            {
                Console.WriteLine(arrItem); //Why does this only print out 2 a's and 2 b's, rather than 4 b's?
            }
            Console.ReadKey();
        }
    }
}

我希望这能概括出技术背景,并解释你的错误期望

c语言中的IEnumerable是懒惰的。这意味着无论何时,只要你强迫它进行评估,你就会得到结果。在您的情况下,
Count()
强制
linqLIST
在每次调用它时进行求值。顺便说一下,
linqLIST
现在不是一个列表

为什么我可以在站点上编辑LINQ列表
private static IEnumerable<T> DontMessWithMe<T>(this T[] source)
{
    var copy = source.ToArray();
    return source.Zip(copy, (x, y) =>
    {
        if (!EqualityComparer<T>.Default.Equals(x, y))
            throw new InvalidOperationException(
            "Array was modified; enumeration operation may not execute.");
        return x;
    });
}
var aArray = new string[] { "a", "a", "a", "a" };
var i = 3;
var linqObj = aArray.Where(x => x == "a");
foreach (var item in linqObj )
{
  aArray[i] = "b";
  i--;
}