32

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

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

LINQ 查询上的慢 foreach()问题演示- ToList() 极大地提高了性能 - 为什么会这样?,这意味着需要调用“ToList()”才能立即评估查询,因为 foreach 正在重复评估数据源上的查询,从而大大减慢了操作速度。

另一个例子是通过分组 linq 结果进行搜索的问题非常慢,有什么提示吗?,其中接受的答案还意味着在查询上调用“ToList()”将提高性能。

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

有问题演示foreach 是否只执行一次查询?, 言下之意是foreach导致一个枚举建立,不会每次都查询数据源。

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

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

4

8 回答 8

20

通常 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 => x.Name投影。之后,您将再次枚举该列表。因此,除非您有充分的理由将数据放在列表中(而不是 IEnumerale),否则您只是在浪费 CPU 周期。

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

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

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

于 2013-06-05T17:35:37.797 回答
8

在 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
于 2013-06-05T17:38:47.527 回答
6

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

于 2013-06-05T17:37:05.623 回答
4

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

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

所以对我来说基本规则是:

  • 如果一个查询仅用于一个查询(仅foreach此而已)-那么我不会缓存该查询
  • 如果在代码中的 aforeach 其他一些地方使用了查询 - 然后我将其缓存在 var 中ToList/ToArray
于 2016-10-06T22:29:24.640 回答
3

foreach,就其本身而言,仅运行一次其数据。事实上,它专门贯穿它一次。您不能向前或向后看,也不能像for循环那样改变索引。

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

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

于 2013-06-05T17:36:04.223 回答
1

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

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

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

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

  • 您不依赖于延迟执行。
  • 您必须访问比整个集合更多的总项目。
  • 您可以支付检索和存储所有项目的前期费用。
于 2013-06-05T17:44:17.123 回答
0

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

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

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

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

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

于 2013-06-05T17:34:12.933 回答
-1

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

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

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

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

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

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

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

这是输出: 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()计时的执行)。在较大的集合上,先缓存集合然后对其进行迭代似乎更快一些,但我的测试没有明确的结论。

于 2018-01-29T13:03:50.577 回答