最近,我遇到了一个问题,在这个问题中,我可以更改IEnumerable循环迭代的对象foreach。据我了解,在C#中,您不应该能够编辑要遍历的列表,但是经过一番挫折之后,我发现这正是发生的事情。我基本上遍历了LINQ查询,并使用对象ID在数据库中对那些对象进行了更改,这些更改影响了.Where()语句中的值。
有人对此有解释吗?似乎每次迭代LINQ查询都会重新运行
注意:此问题的解决方法是.ToList()在之后添加的.Where(),但我的问题是,为什么此问题完全发生,即是错误还是我不知道的事情
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();
}
}
}
Run Code Online (Sandbox Code Playgroud)
这段代码只是一个可复制的模型,但我希望它可以循环4次并将所有字符串都更改aArray为b。但是,它只会循环两次,并将最后两个字符串aArray变成b的
编辑:经过一些反馈和更简洁,我这里主要的问题是:“为什么我能够改变什么,我遍历因为我遍历它。” 似乎压倒性的答案是LINQ确实推迟了执行,因此当我遍历LINQ IEnumerable时它正在重新评估。
编辑2:实际上,似乎所有人都在关注该.Count()功能,认为这就是这里的问题。但是,您可以注释掉该行,而我仍然遇到LINQ对象更改的问题。我更新了代码以反映主要问题
Eri*_*ert 18
为什么在迭代LINQ列表时可以编辑它?
所有表示这是由于延迟的“懒惰”执行而导致的所有答案都是错误的,在某种意义上说,它们没有充分解决所提出的问题:“为什么在迭代列表的同时还能编辑列表?” 延后执行说明了为什么两次运行查询会得到不同的结果,但没有解决为什么问题中所述的操作可行的原因。
问题实际上是原始海报有错误的信念:
最近,我遇到了一个问题,我可以更改在foreach循环中迭代的IEnumerable对象。据我了解,在C#中,您不应该能够编辑要遍历的列表
您的理解是错误的,这就是混乱的根源。C#中的规则不是“不可能从枚举中编辑枚举”。规则是,您不应在枚举中编辑枚举,如果选择这样做,则可能会发生任意坏事。
基本上,您正在执行的操作是运行停车标志,然后询问“运行停车标志是非法的,那么为什么警察不阻止我运行停车标志?” 不需要警察阻止您进行违法行为;您有责任不首先尝试,如果您选择这样做,则有机会获得罚单,或造成交通事故,或因选择不当而造成的其他不良后果。通常,运行停车标志的后果根本没有任何后果,但这并不意味着这是个好主意。
在枚举时编辑可枚举是一种不好的做法,但是并不需要运行时成为交通警察,并且可以防止这样做。也无需将操作标记为非法(带有例外)。它可以这样做,有时也可以这样做,但是并不需要始终如此。
您发现了一种情况,运行时没有检测到问题并且没有引发异常,但是您确实得到了意外发现的结果。没关系。您违反了规则,这一次恰好发生了违反规则的后果是意外的结果。不需要运行时即可将规则分解为异常。
如果您尝试做同样的事情(例如,您在枚举该列表Add的List<T>同时调用),则会出现异常,因为有人在List<T>其中编写了代码来检测这种情况。
没有人为“ linq over a array”编写该代码,因此也不例外。不需要 LINQ的作者编写该代码。您必须不编写自己编写的代码!您选择编写违反规则的错误程序,并且每次编写错误程序时都不需要运行时来捕获您。
似乎每次迭代LINQ查询都会重新运行
那是正确的。查询是关于数据结构的问题。如果您更改该数据结构,则问题的答案可能会更改。枚举查询将回答问题。
但是,这与问题标题中的问题完全不同。您这里确实有两个问题:
您可以执行此不良做法,因为除了您的明智之外,没有什么可以阻止您编写不良程序。编写更好的程序,不要这样做!
是; 查询是一个问题,而不是答案。查询的枚举是一个答案,答案会随着时间而变化。
该解释第一个问题,为什么你LINQ query re-runs every time it's iterated over是因为Linq的延迟执行。
此行仅声明linq exrpession,不执行它:
var linqLIST = aArray.Where(x => x == "a");
Run Code Online (Sandbox Code Playgroud)
这是执行的地方:
foreach (var arrItem in aArray)
Run Code Online (Sandbox Code Playgroud)
和
Console.WriteLine(linqList.Count());
Run Code Online (Sandbox Code Playgroud)
显式调用ToList()将Linq立即运行该表达式。像这样使用它:
var linqList = aArray.Where(x => x == "a").ToList();
Run Code Online (Sandbox Code Playgroud)
关于已编辑的问题:
当然,Linq在每个foreach迭代中都会对表达式求值。问题不在于Count(),而是每次对LINQ表达式的调用都会重新评估它。如上所述,将其枚举为a List并遍历列表。
后期编辑:
关于@Eric Lippert的评论,我还将参考并详细讨论OP的其余问题。
//为什么这只打印2 a和2 b而不是4 b?
在第一个循环迭代中i = 3,因此之后aArray[3] = "b";的数组将如下所示:
{ "a", "a", "a", "b" }
Run Code Online (Sandbox Code Playgroud)
在第二个循环中,迭代i(-)现在的值为2,执行aArray[i] = "b";数组后将为:
{ "a", "a", "b", "b" }
Run Code Online (Sandbox Code Playgroud)
此时,a数组中仍然存在,但是LINQ查询返回IEnumerator.MoveNext() == false,因此循环达到了退出条件,因为IEnumerator内部使用的循环现在到达了数组索引的第三个位置,并且在LINQ重新评估时,它没有x == "a"不再符合where 条件。
为什么在循环播放时可以更改循环播放的内容?
之所以能够这样做,是因为内置代码分析器Visual Studio未检测到您在循环内修改了集合。在运行时,将修改数组,从而更改LINQ查询的结果,但是数组迭代器的实现中没有任何处理,因此不会引发异常。这种缺失的处理在设计上似乎是合理的,因为数组的大小固定为列表的大小,列表在运行时会抛出此类异常。
考虑以下示例代码,该示例代码应与您的初始代码示例等效(在编辑之前):
using System;
using System.Linq;
namespace MyTest {
class Program {
static void Main () {
var aArray = new string[] {
"a", "a", "a", "a"
};
var iterationList = aArray.Where(x => x == "a").ToList();
foreach (var item in iterationList)
{
var index = iterationList.IndexOf(item);
iterationList.Remove(item);
iterationList.Insert(index, "b");
}
foreach (var arrItem in aArray)
{
Console.WriteLine(arrItem);
}
Console.ReadKey();
}
}
}
Run Code Online (Sandbox Code Playgroud)
这段代码将编译并迭代一次循环,然后抛出一条System.InvalidOperationException消息:
Collection was modified; enumeration operation may not execute.
Run Code Online (Sandbox Code Playgroud)
现在,List实现之所以在枚举它时抛出此错误的原因是因为它遵循一个基本概念:For并且Foreach是迭代控制流语句,需要在运行时确定。此外,该Foreach语句是迭代器模式的C#特定实现,该迭代器模式定义了一种隐含顺序遍历的算法,因此它在执行期间不会更改。因此,List在枚举时修改集合时,实现将引发异常。
您发现了一种在每次迭代中迭代和重新展示循环时修改循环的方法。这是一个错误的设计选择,因为如果表达式不断更改结果并且从不满足循环的退出条件,则可能会遇到无限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();
}
}
}
Run Code Online (Sandbox Code Playgroud)
我希望这能概述技术背景并解释您的错误期望。