关于c#:“foreach”会导致Linq重复执行吗?

Does “foreach” cause repeated Linq execution?

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

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

在linq查询上演示了slow foreach()-tolist()极大地提高了性能-这是为什么?,这意味着需要调用"tolist()"以便立即对查询进行计算,因为foreach正在重复对数据源上的查询进行计算,从而大大降低了操作的速度。

另一个例子是,通过分组的LINQ结果预测问题的速度非常慢,有什么建议吗?,其中接受的答案还意味着对查询调用"tolist()"将提高性能。

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

问题中演示了foreach是否只执行一次查询?这意味着foreach将导致建立一个枚举,并且不会每次查询数据源。

继续浏览该站点已经出现了许多问题,其中"foreach循环期间的重复执行"是性能问题的元凶,并且许多其他答案表明foreach将适当地从数据源获取单个查询,这意味着这两种解释似乎都具有有效性。如果"tolist()"假设是错误的(正如目前大多数答案在2013-06-05下午1:51东部标准时间似乎暗示的那样),那么这种误解是从哪里来的?这些解释中是否有一个是准确的,而另一个是不准确的,或者是否有不同的情况会导致LINQ查询的评估不同?

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


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

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

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

如果你做了类似的事情;

1
2
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-prietary数据格式提供程序,就不能保证它以这种方式运行。也许数据的性质会使立即执行成为唯一可行的选择。

另外,最后的编辑;如果你对乔恩·斯基特的《深度C》感兴趣的话,这本书内容丰富,读起来也很棒。我的答案总结了这本书的几页(希望有合理的准确性),但是如果你想了解更多关于Linq如何在封面下工作的细节,那是一个很好的地方。


在LinqPad上试试这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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子句再次运行…可能会导致速度减慢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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查询的使用方式。

1
2
3
4
5
6
7
8
9
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的,每次获取"next"项时都可以使用任何算法。如果您实际上不需要检查整个项目集,这将更快。

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

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


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

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

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

实体框架是一件复杂的事情。当然,有一些方法可以让它重新执行查询,考虑到它在自己的内存模型等中所做的更改。

我建议阅读JuliaLerman的"编程实体框架"。它解决了很多问题,比如你现在遇到的问题。


如果你做.ToList()或不做,则将执行链接声明的次数相同。我这里有一个例子,显示出有色输出到控制台:

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

  • 创建一份100 INTS(0-99)的清单。
  • 创建一个链接声明,从两个*所列的列表打印到红色的控制台上,如果是一个数字,则返回该插件。
  • 在绿色的颜色中打印每一个数字。
  • 在绿色的颜色中打印每一个数字。

当你看到下面的输出时,写入控制台的Ints数字是一样的,意思是链接声明是执行相同的时间。

不同之处在于声明被执行。如你所见,当你在Query(你没有援引.ToList()时,列表和可归还的对象在同一时间被计数。

当你第一次隐瞒名单时,他们总是彼此隔绝,但同样的时间。

区别是非常重要的,因为如果名单在你确定了你的链接声明之后被修改,当它被执行时,链接声明将操作在修改名单上(E.G.by EDOCX1〕〔0〕)。但是,如果你强制执行链接声明(EDOCX1&0),并在之后修改列表,则链接声明将不适用于修改列表。

这里是输出:MGX1〔0〕

这是我的密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 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("
--- {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("
--- {0} FOREACH END ---"
, collectionName);
}

关于执行时间的说明:我进行了几次时间测试(不足以在此处填写),而且在时间上也没有发现任何一致性。在大收藏中,隐藏第一个收藏,然后重新编辑它看起来像是一个比特加速器,但从我的测试中没有明确的结论。