“foreach”会导致Linq重复执行吗?

我第一次使用.NET中的Entity Framework工作,并且一直在编写LINQ查询以便从我的模型中获取信息。 我想从一开始就养成良好的习惯,所以我一直在研究编写这些查询的最佳方法,并得到他们的结果。 不幸的是,在浏览Stack Exchange时,我似乎遇到了延迟/立即执行如何与LINQ一起使用的两个相互矛盾的解释:

  • foreach导致查询在循环的每次迭代中执行:

有问题的certificate在LINQ查询上缓慢的foreach() – ToList()极大地提升了性能 – 为什么会这样? ,暗示需要调用“ToList()”以便立即评估查询,因为foreach正在重复评估数据源上的查询,从而显着降低操作速度。

另一个例子是问题通过分组linq结果进行预测非常缓慢,任何提示? ,其中接受的答案还暗示在查询上调用“ToList()”将提高性能。

  • foreach会导致查询执行一次,并且可以安全地与LINQ一起使用

显示有问题foreach只执行一次查询吗? ,这意味着foreach会导致建立一个枚举,并且每次都不会查询数据源。

继续浏览网站已经出现了很多问题,其中“在foreach循环中重复执行”是性能问题的罪魁祸首,还有很多其他答案表明foreach将从数据源中适当地获取单个查询,这意味着两者都是解释似乎有效。 如果“ToList()”假设不正确(因为美国东部时间2013-06-05 1:51 PM的大部分当前答案似乎都暗示),这种误解来自哪里? 这些解释中是否有一个是准确的,哪个不是,或者是否存在可能导致LINQ查询以不同方式进行评估的不同情况?

编辑:除了下面接受的答案之外,我对程序员提出了以下问题,这非常有助于我对查询执行的理解,特别是在循环期间可能导致多个数据源命中的陷阱,我认为对这个问题感兴趣的人有用: https : //softwareengineering.stackexchange.com/questions/178218/for-vs-foreach-vs-linq

通常,LINQ使用延迟执行。 如果使用First()FirstOrDefault()等方法, FirstOrDefault()立即执行查询。 当你做某事时;

 foreach(string s in MyObjects.Select(x => x.AStringProp)) 

结果以流式方式检索,意味着逐个。 每次迭代器调用MoveNext ,投影都会应用于下一个对象。 如果你有一个它首先应用filter的位置,那么投影。

如果你做的事情;

 List names = People.Select(x => x.Name).ToList(); foreach (string name in names) 

然后我相信这是一个浪费的操作。 ToList()将强制执行查询,枚举People列表并应用x => x.Name投影。 之后,您将再次枚举列表。 因此,除非你有充分的理由将数据放在列表中(而不是IEnumerale),否则你只是在浪费CPU周期。

一般来说,对于使用foreach枚举的集合使用LINQ查询,其性能不会比任何其他类似且实用的选项差。

另外值得注意的是,鼓励实现LINQ提供程序的人员使常用方法像在Microsoft提供的提供程序中那样工作,但他们并不需要这样做。 如果我要编写LINQ to HTML或LINQ to My Proprietary Data Format提供程序,则无法保证它以这种方式运行。 也许数据的性质会使立即执行成为唯一可行的选择。

另外,最后编辑; 如果您对此感兴趣,Jon Skeet的C#In Depth内容非常丰富且非常精彩。 我的回答总结了本书的几页(希望具有合理的准确性),但如果您想了解LINQ如何在幕后工作的更多细节,那么这是一个值得关注的好地方。

在LinqPad上尝试这个

 void Main() { var testList = Enumerable.Range(1,10); var query = testList.Where(x => { Console.WriteLine(string.Format("Doing where on {0}", x)); return x % 2 == 0; }); Console.WriteLine("First foreach starting"); foreach(var i in query) { Console.WriteLine(string.Format("Foreached where on {0}", i)); } Console.WriteLine("First foreach ending"); Console.WriteLine("Second foreach starting"); foreach(var i in query) { Console.WriteLine(string.Format("Foreached where on {0} for the second time.", i)); } Console.WriteLine("Second foreach ending"); } 

每次运行where委托时,我们都会看到一个控制台输出,因此我们可以看到每次都运行Linq查询。 现在通过查看控制台输出,我们看到第二个foreach循环仍然导致“在哪里打印”打印,从而表明foreach的第二次使用实际上导致where子句再次运行…可能导致速度减慢。

 First foreach starting Doing where on 1 Doing where on 2 Foreached where on 2 Doing where on 3 Doing where on 4 Foreached where on 4 Doing where on 5 Doing where on 6 Foreached where on 6 Doing where on 7 Doing where on 8 Foreached where on 8 Doing where on 9 Doing where on 10 Foreached where on 10 First foreach ending Second foreach starting Doing where on 1 Doing where on 2 Foreached where on 2 for the second time. Doing where on 3 Doing where on 4 Foreached where on 4 for the second time. Doing where on 5 Doing where on 6 Foreached where on 6 for the second time. Doing where on 7 Doing where on 8 Foreached where on 8 for the second time. Doing where on 9 Doing where on 10 Foreached where on 10 for the second time. Second foreach ending 

这取决于Linq查询的使用方式。

 var q = {some linq query here} while (true) { foreach(var item in q) { ... } } 

上面的代码将多次执行Linq查询。 不是因为foreach,而是因为foreach在另一个循环中,所以foreach本身正在执行多次。

如果linq查询的所有使用者都“小心”地使用它并避免诸如上面的嵌套循环之类的愚蠢错误,则不应该不必要地多次执行linq查询。

在某些情况下,使用ToList()将linq查询减少到内存中的结果集是合理的,但在我看来,ToList()使用得太频繁了。 每当涉及大数据时,ToList()几乎总是成为毒丸,因为它会强制将整个结果集(可能是数百万行)拉入内存并进行缓存,即使最外层的使用者/枚举器只需要10行。 避免使用ToList(),除非你有一个非常具体的理由,并且你知道你的数据永远不会很大。

如果在代码中多次访问查询, 有时使用ToList()ToArray() “缓存”LINQ查询可能是个好主意。

但请记住,“缓存”它仍然依次称为foreach

所以我的基本规则是:

  • 如果一个查询只是在一个foreach (那就是它) – 那么我就不会缓存查询
  • 如果在foreach 代码中的其他位置使用查询 – 那么我使用ToList/ToArray其缓存在var中

foreach本身只运行一次数据。 事实上,它只运行一次。 你无法向前或向后看,或者以for循环的方式改变索引。

但是,如果代码中有多个foreach ,所有操作都在同一LINQ查询中,则可能会多次执行查询。 但这完全取决于数据 。 如果您正在迭代表示数据库查询的基于LINQ的IEnumerable / IQueryable ,它将每次运行该查询。 如果您正在遍历List或其他objets集合,它将每次都在列表中运行,但不会重复访问您的数据库。

换句话说,这是LINQ的属性,而不是foreach的属性。

不同之处在于基础类型。 由于LINQ构建在IEnumerable(或IQueryable)之上,因此相同的LINQ运算符可能具有完全不同的性能特征。

列表将始终快速响应,但需要预先设置列表。

迭代器也是IEnumerable,并且每次获取“下一个”项时都可以使用任何算法。 如果您实际上不需要完成整套项目,这将更快。

您可以通过在其上调用ToList()并将结果列表存储在局部变量中,将任何IEnumerable转换为列表。 如果这是可取的

  • 您不依赖于延迟执行。
  • 您必须访问比整个集合更多的总项目。
  • 您可以支付检索和存储所有项目的前期费用。

使用LINQ即使没有实体,您将得到的是延迟执行有效。 只有通过强制迭代才能评估实际的linq表达式。 从这个意义上说,每次使用linq表达式时,都会对其进行评估。

现在有了实体,这仍然是相同的,但这里有更多的function。 当entity framework第一次看到表达式时,它会查看是否已经执行了此查询。 如果没有,它将进入数据库并获取数据,设置其内部存储器模型并将数据返回给您。 如果entity framework看到它已经预先获取了数据,那么它将不会转到数据库并使用它先前设置的内存模型将数据返回给您。

这可以让你的生活更轻松,但也可能是一种痛苦。 例如,如果使用linq表达式从表中请求所有记录。 entity framework将加载表中的所有数据。 如果稍后您评估相同的linq表达式,即使在删除或添加记录的时候,您也会得到相同的结果。

entity framework是一件复杂的事情。 当然有一些方法可以让它重新执行查询,同时考虑到它在自己的内存模型中的变化等。

我建议阅读Julia Lerman的“编程entity framework”。 它解决了很多问题,比如你现在拥有的问题。

无论你是否.ToList() 它都将执行相同次数的LINQ语句 。 我在这里有一个示例,彩色输出到控制台:

代码中会发生什么(请参阅底部的代码):

  • 创建100个整数(0-99)的列表。
  • 创建一个LINQ语句,从列表中打印每个int,后跟两个* ,以红色打印到控制台,如果是偶数,则返回int。
  • query做一个foreach,打印出绿色的每个偶数。
  • query.ToList()上做一个foreach,打印出绿色的每个偶数。

正如您在下面的输出中所看到的,写入控制台的int数是相同的,这意味着LINQ语句的执行次数相同。

不同之处在于执行语句的时间 。 正如您所看到的,当您对查询执行foreach(您尚未调用.ToList() )时,同时枚举从LINQ语句返回的列表和IEnumerable对象。

首先缓存列表时,它们是单独枚举的,但仍然是相同的次数。

理解差异非常重要 ,因为如果在定义LINQ语句后修改了列表,LINQ语句将在执行时对修改后的列表进行操作(例如,通过.ToList() )。 但是,如果强制执行LINQ语句( .ToList() )然后修改列表,LINQ语句将.ToList()在修改后的列表上运行。

这是输出: LINQ延迟执行输出

这是我的代码:

 // Main method: static void Main(string[] args) { IEnumerable ints = Enumerable.Range(0, 100); var query = ints.Where(x => { Console.ForegroundColor = ConsoleColor.Red; Console.Write($"{x}**, "); return x % 2 == 0; }); DoForeach(query, "query"); DoForeach(query, "query.ToList()"); Console.ForegroundColor = ConsoleColor.White; } // DoForeach method: private static void DoForeach(IEnumerable collection, string collectionName) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("\n--- {0} FOREACH BEGIN: ---", collectionName); if (collectionName.Contains("query.ToList()")) collection = collection.ToList(); foreach (var item in collection) { Console.ForegroundColor = ConsoleColor.Green; Console.Write($"{item}, "); } Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("\n--- {0} FOREACH END ---", collectionName); } 

关于执行时间的注意事项:我做了一些时序测试(虽然不足以在这里发布)但我没有发现任何一种方法的任何一致性比另一种更快(包括在时间中执行.ToList() )。 在较大的集合中,首先缓存集合然后迭代它似乎更快,但我的测试没有明确的结论。