为什么在迭代LINQ列表时可以编辑它?

Joo*_*337 13 c# linq

最近,我遇到了一个问题,在这个问题中,我可以更改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#中的规则不是“不可能从枚举中编辑枚举”。规则是,您不应在枚举中编辑枚举,如果选择这样做,则可能会发生任意坏事

基本上,您正在执行的操作是运行停车标志,然后询问“运行停车标志是非法的,那么为什么警察不阻止我运行停车标志?” 不需要警察阻止您进行违法行为;您有责任不首先尝试,如果您选择这样做,则有机会获得罚单,或造成交通事故,或因选择不当而造成的其他不良后果。通常,运行停车标志的后果根本没有任何后果,但这并不意味着这是个好主意。

在枚举时编辑可枚举是一种不好的做法,但是并不需要运行时成为交通警察,并且可以防止这样做。也无需将操作标记为非法(带有例外)。它可以这样做,有时也可以这样做,但是并不需要始终如此。

您发现了一种情况,运行时没有检测到问题并且没有引发异常,但是您确实得到了意外发现的结果。没关系。您违反了规则,这一次恰好发生了违反规则的后果是意外的结果。不需要运行时即可将规则分解为异常。

如果您尝试做同样的事情(例如,您在枚举该列表AddList<T>同时调用),则会出现异常,因为有人在List<T>其中编写了代码来检测这种情况。

没有人为“ linq over a array”编写该代码,因此也不例外。不需要 LINQ的作者编写该代码。您必须不编写自己编写的代码!您选择编写违反规则的错误程序,并且每次编写错误程序时都不需要运行时来捕获您。

似乎每次迭代LINQ查询都会重新运行

那是正确的。查询是关于数据结构的问题。如果您更改该数据结构,则问题的答案可能会更改。枚举查询将回答问题。

但是,这问题标题中的问题完全不同。您这里确实有两个问题:

  • 为什么在枚举时可以编辑一个枚举?

您可以执行此不良做法,因为除了您的明智之外,没有什么可以阻止您编写不良程序。编写更好的程序,不要这样做!

  • 每次枚举时,查询是否都会从头开始重新执行吗?

是; 查询是一个问题,而不是答案。查询的枚举是一个答案,答案会随着时间而变化。

  • @Brosto:我建议您花时间来应对* fact *的错误,而不要抱怨* tone *。不幸的是,清楚地陈述事实和清楚地指出其他答案何时是错误的被认为是“侵略性”,而不是正确的改正。我不想让人们对C#抱有错误的信念。纠正他们的错误信念是一种善良。 (10认同)
  • @ Joosh1337:当您遍历数组而不是数据库时,您的代码会更改数组中的对象,并询问-两次-为什么在编辑列表时可以编辑列表?如果这不是您想要回答的问题,那不是您应该提出的问题!这里没有“骚动”的意图;您的问题暗示您有许多错误信念。我希望您成为一名成功的C#程序员,并且成功的C#程序员不要对语言有错误的信念! (9认同)
  • @Brosto:我不是在“分裂头发”;我正在掩盖他们的错误信念的原始海报。认为“您不应该能够编辑要遍历的列表”是“错误的”。正确的陈述是“您不应该编辑要遍历的列表”,而这些则是“非常不同的陈述”。一个隐含着对“运行时”的“要求”,另一个隐含着对“程序作者”的一个要求。当您尝试编写正确的程序时,正确区分这一点至关重要。 (5认同)
  • @Servy:他们的错不是从他们对延期执行的存在不正确的意义上说是错误的,而是因为延期执行解释了原始海报的混乱;我认为答案中包含正确的陈述,但“有误导性”意味着它把问题的根源*称为“错误答案”。现在,您可能会争辩说,最好是说出“无关紧要”或“不合理的推理”,或者说“错误”以外的其他词,但这再次是对语气的回应。 (5认同)
  • @Servy:您的第二点评论是我在此答案中试图传达的确切内容的摘要;感谢您的总结。**违反约定可能导致意外的结果,但并不能确定编写违反约定的程序的可能性**。 (5认同)
  • 现在,关于语气,我鼓励大家再次阅读答案。每个句子都是陈述事实的简单明了的陈述性句子。如果您阅读简单,直截了当的陈述性句子,将事实表述为“具有攻击性”和“令人发指”,那么我鼓励您首先,更善于阅读,其次,问自己为什么对事实陈述有情感上的反应。 (5认同)
  • 到目前为止,此答案针对的是其他答案未解决的中心问题,但由于其语气(事实和明确的观点)而受到批评,并且基于我不理解该观点的毫无根据的建议而受到批评。题。到目前为止,我所看到的唯一有效的批评是缺乏简洁性,这是我完全同意的,并且它解决了原始海报实际上没有持有的错误信念,在这种情况下,问题应该更清楚了。我只会根据答案的准确性来回应批评。 (5认同)
  • “所有说这是由于推迟执行“懒惰”的答案都是错误的。” 为何如此?“ Where”在被请求时会立即评估每个项目的事实恰好是一种机制,它允许*在迭代过程中观察到更改,这就是所要提出的问题。问题作者并不认为这是不可能的,因为存在一些通用规则,即所有迭代器在其基础数据源发生更改时都必须抛出该异常,他们希望在更改之前看到这些值,因为他们认为LINQ方法返回查询的结果,而不是查询的结果。一个问题。 (4认同)
  • 另外,没有“规则”,即不允许IEnumerator对象在迭代基础数据源时观察任何基础数据源的变化。这是一个*惯例*,*通常*是一个好主意,但在某些情况下,这样做反而很有用。某些类型的更改使打开的迭代器无法明智地继续下去,或者非常困难。其他类型的更改不会对开放迭代器产生负面影响。如果操作不当,可能会造成混淆或产生无效结果。但这与为什么可行的问题没有任何关系。 (3认同)
  • @Servy:我正在回答所问的问题,即“为什么在迭代LINQ列表时我能编辑它?” 我正在解决原始海报直接指出的一个误解:“据我了解,在C#中,您不应该能够编辑要遍历的列表”。我不明白为什么这会引起争议;我们应该回答所提出的问题,并且应该使人们不相信他们的错误信念;这就是该站点提供的服务。 (3认同)
  • @Servy:所有这些对我来说都是清楚的,也是一个问题,那就是“为什么我要在遍历它的同时编辑LINQ列表?”,以及不正确的信念“这是我的理解,在C#中,您不应该能够编辑您要遍历的列表”,我在回答中都提到了这两个问题。我不确定您为什么仍在继续进行此操作;这对我来说很清楚。我在LINQ上有一些经验,人们对它有误解,这是世界上最长久以来一直在消除人们的误解的人。 (3认同)
  • 这是我在回答中提到的问题,所以我不确定您的批评有什么可行的。其他答案的核心问题是,他们*仅*解决了所关注的问题,而不是着眼于问题的症结,这是因为原始海报似乎不正确地认为在编辑枚举时可能反复进行。有人声称我有些不了解的人是* you *,而不是* me *;我并不是说我是对的;我是说您有充分的理由知道我理解这个问题。 (3认同)
  • @Servy:当某人认为某事“不可能”并问“为什么可能?”时,根本的问题不在于对“机制”的理解不深-尽管我同意这是一个解释机制的机会。根本问题是关于“应该或不应该”的错误信念。那就是要解决的问题!现在,如果原始海报(如他们所说)*实际上*并没有这种错误的信念,那么应该更清楚地说明问题。 (2认同)
  • 重新阅读您的答案后,我决定扩展我的答案以充实,并作为补充说明,尽管您的答案反复强调这是一种不好的做法,但并不能解决为什么这样做的问题,也无法回答他没有输出的原因4 b在他的代码示例中。 (2认同)

Rau*_*ian 8

该解释第一个问题,为什么你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)

我希望这能概述技术背景并解释您的错误期望。

  • @EricLippert的答案仅说明了代码的行为。您的陈述是正确的,并解释了他的困惑,我没有判断他的代码。有很多技巧可以使C#表现出意外。如果他会使用List并尝试在迭代中操作该集合,那么在编译之前它会被拦截,但是正如您所说的,有几个逻辑问题无法分析。无论哪种方式,他现在都应该能够理解问题 (2认同)