通常返回序列的方法使用延迟执行:
IEnumerable<X> ---> Select ---> IEnumerable<Y>
而返回单个对象的方法则不会:
IEnumerable<X> ---> First ---> Y
所以,像 Where
、Select
、Take
、Skip
、GroupBy
和 OrderBy
这些方法使用延迟执行是因为它们可以,而像 First
、Single
、ToList
和 ToArray
这些方法不使用延迟执行是因为它们不能。
此外,还有两种延迟执行的类型。例如,Select
方法在被要求生成一个项目时只会逐个获取一个项目,而当被要求返回第一个项目时,OrderBy
方法必须消耗整个源。因此,如果你在 Select
后面链接了一个 OrderBy
,则执行将被推迟直到获取第一个项目,但然后 OrderBy
将要求 Select
返回所有项目。
始终假设返回 IEnumerable<T>
或者 IQueryable<T>
的任何 API 都可以且可能会使用延迟执行。如果你需要对结果进行多次迭代(例如获取 Count),那么在这样的 API 上进行消费之前,请先将其转换为集合(通常通过调用 .ToList() 扩展方法来实现)。
如果您正在公开一个枚举对象,则始终将其公开为一个集合(ICollection<T>
或 IList<T>
),如果这是您的客户端通常使用的方式。例如,数据访问层通常会返回一组域对象的集合。只有当延迟执行对于您要公开的 API 是一个合理的选择时,才公开 IEnumerable<T>
。
实际上,还有更多需要考虑的,除了排序之外,您还需要考虑缓冲和非缓冲。OrderBy可以延迟执行,但是在迭代时必须消耗整个流。
通常,在LINQ中返回IEnumerable的任何内容都倾向于被延迟执行,而返回值的Min等则不是延迟执行的。缓冲(与否)通常可以推理出来,但老实说,反编译器是确定的一种相当快速的方法。但请注意,这通常是一个实现细节。
如果需要实现“延迟执行”,您需要使用基于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中,并对其进行处理,从而强制通过所有更高节点评估所有元素。通常情况下,如果可以的话,您希望这些操作在方法链的后面出现。
如果您正在使用查询表达式语法而非查询方法语法,则查询将被延迟执行。
如果您正在使用查询方法语法,则根据其返回内容,查询可能会被延迟执行。
将鼠标悬停在变量类型(如果您使用的是用于存储查询的变量类型)的var
关键字上。如果显示IEnumerable<T>
,则查询将被延迟执行。
尝试使用foreach遍历查询。如果出现错误,指示无法迭代变量,因为它不支持GetEnumerator()
,则说明查询不会被延迟执行。