为什么在遍历LINQ列表时可以编辑它?

时间:2019-04-30 14:47:25

标签: 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();
        }
    }
}

此代码只是可复制的模型,但我希望它可以循环4次并将aArray中的所有字符串更改为b。但是,它只会循环两次,并将aArray中的最后两个字符串转换为b的

编辑:经过一些反馈并且更加简洁,我在这里的主要问题是:“为什么我要更改我正在遍历 ?似乎压倒性的答案是LINQ确实推迟了执行,因此在我遍历LINQ IEnumerable时正在重新评估。

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

5 个答案:

答案 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实现在枚举该错误时会抛出此错误的原因是,它遵循一个基本概念:ForForeach迭代控制流语句 strong>,需要在运行时确定性。此外,Foreach语句是deferred executionC#特定实现,它定义了一种算法,该算法隐含顺序遍历,因此在执行期间不会更改。因此,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)

C#中的

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--;
}
  • 在foreach循环中,将枚举linqObj *并开始查询。
  • 检查了第一项并找到了一个匹配项。查询已暂停。
  • 发生循环主体:item="a", aArray[3]="b", i=2
  • 返回到foreach循环,恢复查询。
  • 检查了第二项并找到了匹配项。查询已暂停。
  • 发生循环主体:item="a", aArray[2]="b", i=2
  • 返回到foreach循环,恢复查询。
  • 第三个项目被检查为“ b”,而不是匹配项。
  • 第四个项目被检查为“ b”,而不是匹配项。
  • 退出循环,查询结束。

注意:是枚举*:这意味着调用了GetEnumerator和MoveNext。这并不意味着对查询进行了完全评估,并且结果保留在快照中。

为进一步理解,请阅读yield return以及如何编写使用该语言功能的方法。如果这样做,您将了解编写Enumerable.Where

所需的内容