我最近遇到了一个问题,即我可以更改在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();
}
}
}
此代码只是可复制的模型,但我希望它可以循环4次并将aArray
中的所有字符串更改为b。但是,它只会循环两次,并将aArray
中的最后两个字符串转换为b的
编辑:经过一些反馈并且更加简洁,我在这里的主要问题是:“为什么我要更改我正在遍历的 ?似乎压倒性的答案是LINQ确实推迟了执行,因此在我遍历LINQ IEnumerable时正在重新评估。
编辑2:实际上,实际上,似乎每个人都在关注.Count()
函数,认为这是这里的问题所在。但是,您可以注释掉该行,而我仍然遇到LINQ对象更改的问题。我更新了代码以反映主要问题
答案 0 :(得分:17)
为什么在迭代LINQ列表时可以编辑它?
所有表示这是由于延迟执行“懒惰”的答案都是错误的,在某种意义上说,它们并不能充分解决所提出的问题:“为什么在迭代列表时为什么能编辑列表? ?延后执行说明了为什么两次运行查询会得到不同的结果,但没有解决问题中所述的操作为什么可能的原因。
问题实际上是原始海报有错误的信念:
我最近遇到了一个问题,我可以更改在foreach循环中迭代的IEnumerable对象。据我了解,在C#中,您不应该能够编辑要遍历的列表
您的理解是错误的,这就是困惑的源头。 C#中的规则不是“不可能从枚举中编辑枚举”。规则是您不应在枚举中编辑枚举,如果选择这么做,则可能会发生任意坏事。
基本上您在做什么是运行停车牌,然后询问“运行停车牌是非法的,那么为什么警察不阻止我运行停车牌?”不需要警察阻止您进行违法行为; 您有责任不首先尝试,如果您选择这样做,则有机会获得罚单,或造成交通事故,或因选择不当而造成的任何其他不良后果。 / strong>通常,运行停车标志不会带来任何后果,但这并不意味着它是个好主意。
在枚举枚举时编辑枚举是一种不好的做法,但是并不一定要让运行时成为交通警察,并且防止这样做。也无需将操作标记为非法(带有例外)。它可以这样做,有时甚至可以这样做,但是没有要求如此一致。
您发现一种情况,运行时未检测到问题并且未引发异常,但是您确实得到了意外发现的结果。没关系。您违反了规则,这一次恰好发生了违反规则的后果是意外的结果。不需要运行时即可将规则分解为异常。
如果您尝试做同样的事情,例如在枚举列表时在Add
上调用了List<T>
,那么您会遇到一个异常,因为有人在List<T>
中编写了代码可以检测到这种情况。
没有人为“ linq over a array”编写该代码,因此也不例外。不需要LINQ的作者来编写该代码。您必须不编写自己编写的代码!您选择编写一个违反规则的错误程序,并且每次编写错误程序时都不需要运行时来捕获您。
似乎LINQ查询在每次迭代时都会重新运行
是正确的。查询是关于数据结构的问题。如果您更改该数据结构,则问题的答案可能会更改。枚举查询将回答问题。
但是,这是与问题标题中的问题完全不同的问题。您这里确实有两个问题:
您可以执行此不良做法,因为除了您的明智之外,没有什么可以阻止您编写不良程序。编写更好的程序,做到这一点!
是;查询是一个问题,而不是答案。查询的枚举是一个答案,答案可能会随时间变化。
答案 1 :(得分:8)
第一个问题的解释,为什么您的LINQ query re-runs every time it's iterated over
是由于Linq
的{{3}}而引起的。
此行仅声明linq表达式,不执行:
var linqLIST = aArray.Where(x => x == "a");
这是执行的地方:
foreach (var arrItem in aArray)
和
Console.WriteLine(linqList.Count());
显式调用ToList()
将立即运行Linq
表达式。像这样使用它:
var linqList = aArray.Where(x => x == "a").ToList();
关于已修改的问题:
当然,Linq
表达式是在每个 foreach 迭代中求值的。问题不是Count()
,而是每次对LINQ表达式的调用都会重新评估它。如上所述,将其枚举为List
并遍历列表。
最新修改:
关于 @Eric Lippert 的评论,我还将参考并详细讨论OP的其余问题。
//为什么这只打印2 a和2 b而不是4 b?
在第一个循环迭代i = 3
中,因此在aArray[3] = "b";
之后,您的数组将如下所示:
{ "a", "a", "a", "b" }
在第二个循环迭代中,i
(-)现在具有值 2 ,执行aArray[i] = "b";
后,您的数组将为:
{ "a", "a", "b", "b" }
此时,您的数组中仍然有a
,但是LINQ
查询返回IEnumerator.MoveNext() == false
,因此循环达到了退出状态,因为IEnumerator
在内部使用,现在到达数组索引的第三个位置,并且在重新评估LINQ
时不再匹配x == "a"
的条件。
为什么在循环播放时可以更改循环播放的内容?
之所以能够这样做,是因为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();
}
}
}
此代码将编译一次并循环一次,然后抛出System.InvalidOperationException
,并显示以下消息:
Collection was modified; enumeration operation may not execute.
现在,List
实现在枚举该错误时会抛出此错误的原因是,它遵循一个基本概念:For
和Foreach
是迭代控制流语句 strong>,需要在运行时确定性。此外,Foreach
语句是deferred execution的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();
}
}
}
我希望这可以概述技术背景并解释您的错误期望。
答案 2 :(得分:2)
IEnumerable
很懒。这意味着只要您强迫它进行评估就可以得到结果。对于您的情况,Count()
会强制linqLIST
在每次调用时进行评估。顺便说一句,linqLIST
暂时不是列表
答案 3 :(得分:2)
通过使用以下扩展方法,您可以将«枚举数组时避免副作用»建议升级为要求:
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 linqObj = aArray.DontMessWithMe().Where(x => x == "a");
当然,这是有代价的。现在,每次枚举数组时,都会创建一个副本。这就是为什么我不希望任何人会使用此扩展程序的原因!
答案 4 :(得分:2)
Enumerable.Where
返回代表查询定义的实例。枚举*时,对查询进行评估。 foreach
使您可以在查询中找到每个项目 。该查询被枚举机制推迟,但也可以暂停/恢复。
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--;
}
item="a", aArray[3]="b", i=2
item="a", aArray[2]="b", i=2
注意:是枚举*:这意味着调用了GetEnumerator和MoveNext。这并不意味着对查询进行了完全评估,并且结果保留在快照中。
为进一步理解,请阅读yield return
以及如何编写使用该语言功能的方法。如果这样做,您将了解编写Enumerable.Where