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

时间:2013-06-05 17:27:20

标签: c# .net linq

我第一次使用.NET中的Entity Framework工作,并且一直在编写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-06-05 1:51 PM的大部分当前答案似乎都暗示),这种误解来自何处?这些解释中是否有一个是准确的,哪个不是,或者是否存在可能导致LINQ查询以不同方式进行评估的不同情况?

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

8 个答案:

答案 0 :(得分:17)

通常,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写入HTML或LINQ to My Proprietary Data Format提供程序,则无法保证它以这种方式运行。也许数据的性质会使立即执行成为唯一可行的选择。

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

答案 1 :(得分:6)

在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

答案 2 :(得分:4)

这取决于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(),并且你知道你的数据永远不会很大。

答案 3 :(得分:3)

foreach本身只运行一次数据。事实上,它具体贯穿它一次。您无法向前或向后查看,也无法使用for循环更改索引。

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

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

答案 4 :(得分:3)

有时对于&#34;缓存&#34;可能是一个好主意。如果在您的代码中多次访问查询,则使用ToList()ToArray()的LINQ查询。

但请记住&#34;缓存&#34;它仍然依次调用foreach

所以我的基本规则是:

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

答案 5 :(得分:1)

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

列表总是能够快速响应,但需要花费大量精力来构建列表。

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

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

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

答案 6 :(得分:0)

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

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

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

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

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

答案 7 :(得分:-1)

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

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

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

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

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

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

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

这是输出: LINQ Deferred Execution output

这是我的代码:

// 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()的时间)。在较大的集合中,首先缓存集合然后迭代它似乎更快,但我的测试没有明确的结论。