【问题标题】:Does "foreach" cause repeated Linq execution?“foreach”会导致 Linq 重复执行吗?
【发布时间】:2013-06-01 12:34:24
【问题描述】:

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

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

在问题Slow foreach() on a LINQ query - ToList() boosts performance immensely - why is this? 中显示,这意味着需要调用“ToList()”才能立即评估查询,因为 foreach 正在重复评估数据源上的查询,从而大大减慢了操作速度。

另一个例子是问题 Foreaching through grouped linq results is incredibly slow, any tips? ,其中接受的答案还暗示在查询上调用“ToList()”将提高性能。

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

在问题Does foreach execute the query only once?中演示,意思是foreach导致一个枚举建立,不会每次都查询数据源。

继续浏览该网站发现了许多问题,其中“在 foreach 循环期间重复执行”是性能问题的罪魁祸首,还有许多其他答案表明 foreach 将适当地从数据源中获取单个查询,这意味着这两种解释似乎都有效。如果“ToList()”假设不正确(截至 2013 年 6 月 5 日下午 1:51 EST 的大多数当前答案似乎暗示),这种误解来自哪里?这些解释中是否有一种准确而另一种不准确,或者是否存在可能导致 LINQ 查询评估不同的不同情况?

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

【问题讨论】:

  • 我想这取决于 foreach 中的查询实际上在做什么。
  • foreach 通常是与IEnumerable/IQueryable 一起使用时出现性能问题的罪魁祸首。
  • 我们真的需要一个非常具体的例子才能正确地推理它。 foreach 只会调用GetEnumerator 一次...但是如果您多次执行整个foreach 循环,它将多次调用GetEnumerator...
  • 链接的问题是可疑的,我不相信那里接受的答案。
  • 回答编辑过的问题:这与 foreach 无关,而与延迟执行 LINQ 无关。如果您反复使用.Contains().First().Single() 或其他任何导致 LINQ 执行的东西,您会遇到完全相同的问题。对于对象,这无关紧要,但对于数据库查询,它确实如此。调用ToList() 将查询(如果是一个)转换为对象,因此以后使用不需要命中数据库。

标签: c# .net linq


【解决方案1】:

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

代码中发生了什么(见底部代码):

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

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

区别在于语句的执行时间。如您所见,当您对查询执行 foreach 时(您尚未调用 .ToList() on),同时枚举从 LINQ 语句返回的列表和 IEnumerable 对象。

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

理解的区别很重要,因为如果列表在你定义了你的LINQ语句之后被修改了,LINQ语句在执行的时候会对修改后的列表进行操作(例如@987654328) @)。但是,如果您强制执行 LINQ 语句 (.ToList()) 然后修改列表,则 LINQ 语句将无法在修改后的列表上运行。

这是输出:

这是我的代码:

// Main method:
static void Main(string[] args)
{
    IEnumerable<int> 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<int> 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() 的时间)。在较大的集合上,先缓存集合然后对其进行迭代似乎更快一些,但我的测试没有明确的结论。

【讨论】:

    【解决方案2】:

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

    但请记住,“缓存”它仍会依次调用foreach

    所以我的基本规则是:

    • 如果一个查询仅在一个 foreach 中使用(仅此而已) - 那么我不会缓存该查询
    • 如果在代码中的 foreach 和中使用了查询 - 然后我使用 ToList/ToArray 将其缓存在 var 中

    【讨论】:

    • 也许“buffer”、“eager execution”,或者像您使用的那样,“cache”会比“serialize”更好?
    【解决方案3】:

    在 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 循环仍然会导致“Doing where on”打印,从而表明 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
    

    【讨论】:

    • 请在您的回答中描述这应该表明什么。
    • 每次运行 where 委托时,我们都会看到控制台输出,因此我们可以看到每次运行的 Linq 查询。现在通过查看控制台输出,我们看到第二个 foreach 循环仍然会导致“Doing where on”打印,从而表明 foreach 的第二次使用确实会导致 where 子句再次运行......可能会导致速度变慢.
    • 您应该在答案中包含这一点,而不是仅仅说“试试这个”。
    • 我只是觉得这有点不言自明……我尽力在代码中而不是用 cmets 来描述我在做什么……
    • 代码中不需要在cmets中描述。它只需要一两句话说为什么你建议运行这个,所以人们知道它是否值得尝试。感谢您添加它 - 已删除反对票。
    【解决方案4】:

    通常 LINQ 使用延迟执行。如果您使用First()FirstOrDefault() 之类的方法,则会立即执行查询。当你做类似的事情时;

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

    结果以流式方式检索,即逐一检索。每次迭代器调用MoveNext 时,投影都会应用于下一个对象。如果你有一个Where,它将首先应用过滤器,然后是投影。

    如果你做类似的事情;

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

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

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

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

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

    【讨论】:

    • 感谢推荐书。这似乎证实了我自己的摆弄以及我出现的文章的普遍共识。
    • 请注意,当您在枚举器上调用Current 时,投影是应用的,当您调用MoveNext 时应用它。 Current 只是获取调用MoveNext 时生成的值。这很好,这意味着您无需担心在调用 MoveNext 之间多次使用 Current
    • @Servy 感谢您的更正。如果您愿意,请随时编辑帖子。
    • 如果 LINQ 语句使用 OrderBy 或类似的枚举整个集合怎么办?例如foreach (var thing in things.OrderBy(r => r.Order).ToArray()) 在 for 循环中每次迭代执行一次还是一次?
    • 我相信您对“浪费操作”的看法是错误的。如果您查看我对问题的回答,您可以看到枚举发生了两次。
    【解决方案5】:

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

    列表总是能快速响应,但构建列表需要前期努力。

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

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

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

    【讨论】:

      【解决方案6】:

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

      现在对于实体,这仍然是相同的,但这里有更多功能在起作用。 当实体框架第一次看到这个表达式时,它会查看他是否已经执行了这个查询。如果没有,它将进入数据库并获取数据,设置其内部内存模型并将数据返回给您。如果实体框架看到它已经预先获取了数据,它就不会去数据库并使用它之前设置的内存模型来向您返回数据。

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

      实体框架是一个复杂的东西。当然有办法让它重新执行查询,同时考虑到它在自己的内存模型等方面的变化。

      我建议阅读 Julia Lerman 的“编程实体框架”。它解决了很多问题,例如您现在遇到的问题。

      【讨论】:

      • 所有正确但没有解释为什么 ToList() 应该比 foreach() 快。它可能永远不会。
      • 我也不认为 foreach 会比 ToList 慢。
      【解决方案7】:

      这取决于 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(),除非您有非常具体的理由并且您知道您的数据永远不会很大。

      【讨论】:

        【解决方案8】:

        foreach 本身只运行一次它的数据。事实上,它专门贯穿它一次。您不能向前或向后看,也不能像使用 for 循环那样更改索引。

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

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

        【讨论】:

          猜你喜欢
          • 2020-11-04
          • 1970-01-01
          • 2023-04-01
          • 1970-01-01
          • 2012-08-24
          • 2021-07-28
          • 1970-01-01
          • 1970-01-01
          • 2018-08-02
          相关资源
          最近更新 更多