Linq - 如何快速确定是否存在延迟执行?

32
如何快速查找哪些 .net framework 的 linq 方法(例如 IEnumerable linq 方法)使用延迟执行,而哪些不是。在编码时,我经常会想知道代码是否能正确执行,唯一的方法是去 MSDN 文档中查看。有没有更快的方法,比如网上的目录、列表、备忘单或其他技巧,可以帮助像我这样的 Linq 新手减少错误?如果有,请分享一下。另外一个选择是检查文档,直到记住了它们(这对我来说很难,我不太容易记住已记录在某处并且可以查找的“任何东西” :D)。
6个回答

38

通常返回序列的方法使用延迟执行:

IEnumerable<X> ---> Select ---> IEnumerable<Y>

而返回单个对象的方法则不会:

IEnumerable<X> ---> First ---> Y

所以,像 WhereSelectTakeSkipGroupByOrderBy 这些方法使用延迟执行是因为它们可以,而像 FirstSingleToListToArray 这些方法不使用延迟执行是因为它们不能。

此外,还有两种延迟执行的类型。例如,Select 方法在被要求生成一个项目时只会逐个获取一个项目,而当被要求返回第一个项目时,OrderBy 方法必须消耗整个源。因此,如果你在 Select 后面链接了一个 OrderBy,则执行将被推迟直到获取第一个项目,但然后 OrderBy 将要求 Select 返回所有项目。


2
将“collection”替换为“sequence”我会同意,但是集合(List<T>等)通常不是延迟的。 - Marc Gravell
@enzero:如果您修正语法,请不要删除文本的任何部分。 - Guffa
@Enzero:你认为什么不合理? - Guffa
@Guffa "不要因为他们不能而不去做",两个否定词连在一起读起来不太对。 - Enzero
@Guffa 在那个意义上,这听起来更好。无论如何,重要的是最终的答案。答案非常清晰明了。 - Enzero
显示剩余3条评论

8
我使用的准则如下:
  • 始终假设返回 IEnumerable<T> 或者 IQueryable<T> 的任何 API 都可以且可能会使用延迟执行。如果你需要对结果进行多次迭代(例如获取 Count),那么在这样的 API 上进行消费之前,请先将其转换为集合(通常通过调用 .ToList() 扩展方法来实现)。

  • 如果您正在公开一个枚举对象,则始终将其公开为一个集合(ICollection<T>IList<T>),如果这是您的客户端通常使用的方式。例如,数据访问层通常会返回一组域对象的集合。只有当延迟执行对于您要公开的 API 是一个合理的选择时,才公开 IEnumerable<T>


5

实际上,还有更多需要考虑的,除了排序之外,您还需要考虑缓冲和非缓冲。OrderBy可以延迟执行,但是在迭代时必须消耗整个流。

通常,在LINQ中返回IEnumerable的任何内容都倾向于被延迟执行,而返回值的Min等则不是延迟执行的。缓冲(与否)通常可以推理出来,但老实说,反编译器是确定的一种相当快速的方法。但请注意,这通常是一个实现细节。


2

如果需要实现“延迟执行”,您需要使用基于IQueryable的方法。基于IQueryable的方法可以构建表示查询的表达式树。只有当您调用一个接受IQueryable并生成具体或IEnumerable结果(如ToList()和类似方法,AsEnumerable()等)的方法时,Linq提供程序(Linq2Objects内置于框架中,Linq2SQL和MSEF现在也是如此;其他ORM和持久层框架也提供Linq提供程序)才会评估该树并返回实际结果。框架中的任何IEnumerable类都可以使用AsQueryable()扩展方法转换为IQueryable,并且像ORM这样将翻译表达式树的Linq提供程序将提供AsQueryable()作为针对其数据的Linq查询的起点。

即使对于IEnumerable,一些Linq方法也是“惰性”的。因为IEnumerable的优美之处在于您不必了解其中的全部内容,只需了解当前元素以及是否还有另一个元素,所以作用于IEnumerable的Linq方法通常返回一个迭代器类,该类在链中后续方法请求对象时从其源发出一个对象。任何不需要知道整个集合的操作都可以被惰性地评估(Select和Where是两个重要的操作;还有其他操作)。那些需要知道整个集合的操作(例如OrderBy排序、GroupBy分组以及Min和Max等聚合操作)将把它们的整个源可枚举对象读入到List或Array中,并对其进行处理,从而强制通过所有更高节点评估所有元素。通常情况下,如果可以的话,您希望这些操作在方法链的后面出现。


1
我不同意AsQueryable在这里改变了任何东西的建议;本质上,这只是在表达式树被编译并传递给Enumerable时添加了一层工作。表达式树仅用于ORM(等)源的检查。LINQ-to-objects只是编译它们并调用委托。它不会改变关于延迟或缓冲的任何事情。 - Marc Gravell
2
说真的,迭代器块很容易编写,并且完全是延迟的。LINQ-to-objects 主要使用迭代器块。 - Marc Gravell

2
以下是查询是否延迟执行的不同方式总结:
  1. 如果您正在使用查询表达式语法而非查询方法语法,则查询将被延迟执行。

  2. 如果您正在使用查询方法语法,则根据其返回内容,查询可能会被延迟执行。

  3. 将鼠标悬停在变量类型(如果您使用的是用于存储查询的变量类型)的var关键字上。如果显示IEnumerable<T>,则查询将被延迟执行。

  4. 尝试使用foreach遍历查询。如果出现错误,指示无法迭代变量,因为它不支持GetEnumerator(),则说明查询不会被延迟执行。

来源:Essential Linq

1
如果你使用 .AsQueryable() 将集合转换为 IQueryable,那么你的 LINQ 调用将使用延迟执行。
请参见:在 Linq 中使用 IQueryable

1
调用AsQueryable不会改变这一点,它需要成为内容的适当数据源才能帮助; 即使隐藏在IQueryable后面,LINQ to Objects仍然是LINQ to objects。 - Marc Gravell
1
@Marc:不是这样的。IQueryable方法在必要时才进行评估,而与原始源无关。任何链接到AsQueryable()并只输出另一个IQueryable的内容基本上什么也没有做,只是添加了一个节点到表达式树中。现在,如果你在末尾加上ToList(),那么树就会在构建后立即被评估;它必须这样做,以便立即给出你所需的结果。这都取决于你自己;根据情况,你实际上可以通过等待需要一个List来调用IQueryable.ToList()来利用延迟执行。 - KeithS
3
@Keith 你所说的一切也适用于“迭代器块”。大多数LINQ-to-objects使用迭代器块(yield return)。 - Marc Gravell
关键短语在链接的答案中是“与正确支持它的提供程序一起使用”。AsQueryable不会对解析/解释表达式树做任何特殊处理 - 它只是将它们(作为委托)传递给Enumerable。它们仍然可以被延迟,但是AsQueryable实际上在这里添加了工作。 - Marc Gravell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接