IEnumerable与IQueryable形式的数据源之间的区别

IQueryable与IEnumerable这两个类型在API签名上面很像,而且前者继承自后者,因此,有人觉得它们可以互换。没错,在很多情况下确实如此,而且这正是API设计者想要的效果。但若从序列的角度来看,则这两者并不完全相同,因为它们的行为毕竟是有所区别的,而且这种区别可能会极大地影响程序的性能。比方说,下面这两条查询语句就很不一样:

尽管它们返回的结果相同,但其工作方式却大有不同。第一种写法采用的是IQueryable所内置的LINQ to SQL机制,而第二种写法则是把数据库对象强制转为IEnumerable形式的序列,并把排序等工作放在本地完成。这相当于是将IQueryable所提供的LINQ to SQL机制与惰性求值结合起来。

等到用户想要遍历查询结果的时候,LINQ to SQL程序库会把相关的查询操作合起来执行,并给出查询结果。具体到本例来说,这意味着只需向数据库发出一次调用即可,而且where子句与orderby子句也能在同一次SQL查询操作里面完成。

第二种写法会把经过where子句所过滤的结果转成IEnumerable型的序列,并采用LINQ to Objects机制来完成后续的操作,那些操作会通过委托得以执行。也就是说,它会先向数据库发出请求,把位于伦敦的那些顾客获取过来,然后在获取到的数据集上面根据顾客的名字来排序,这意味着排序操作其实是在本地而不是在远端执行的。

你需要注意这种区别,因为有些功能用IQueryable实现起来要比用IEnumerable快得多。此外,你还应该了解IQueryable与IEnumerable会分别采用怎样的方式来处理查询表达式,因为某些写法只适用于其中一种环境,而不能在另一种环境下正常运作。

由于两者会用不同的类型来表示处理过程中所涉及的数据,因此,它们所依循的其实是两套完全不同的流程。无论是用lambda表达式来撰写查询逻辑还是以函数参数的形式来表示这些逻辑,针对IEnumerable所设计的那些扩展方法都会将其视为委托。反之,针对IQueryable的那些扩展方法用的则是表达式树(expression tree),这种数据结构可以把各种逻辑合起来构建成一条查询操作。用IEnumerable所写的那个版本必须在本地执行,系统要把lambda表达式编译到方法里面,并在本地计算机上面运行,这意味着无论有待处理的数据在不在本地,都必须先获取过来才行。如果这些数据放在远端,那么可能要通过网络传输大量的信息,而且传输过来之后,还得把不需要的那些信息删掉。

用IQueryable实现出来的版本则会解析表达式树,在解析的时候,系统会把这棵树所表示的逻辑转换成provider能够操作的格式,并将其放在离数据最近的地方去执行。这意味着需要传输的数据会远远少于IEnumerable版本,而且总体性能也更好。然而,在IQueryable型的序列上面查询是会受到一些限制的,因为并不是所有的操作都可以出现在查询表达式中,只有那些为底层实现所支持的操作才可以写入表达式。

本章早前的第37条说过,用来支持IQueryable的那些provider未必能够解析每一种查询方法,因为假如要保证每个方法都能得到解析,那就得把用户有可能写出的每一种逻辑全都考虑进来。实际上,那些provider只能解读某几种固定的运算符,而且有可能只支持某一套固定的方法,那些方法是.NET Framework已经实现好的。如果你要在查询操作里面调用除此之外的其他方法,那么可能得把序列当成IEnumerable来查询(而不能直接以IQueryable的形式查询)。

第一种写法能够正常运作,因为LINQ to Objects会以委托的形式将这些查询操作实现成方法调用。用了AsEnumerable()方法之后,查询工作就必须在本地用户所处的环境中执行,而且where子句内的逻辑也要由LINQ to Objects来处理。第二种写法会抛出异常,因为LINQ to SQL是用IQueryable来实现查询的,而LINQ to SQL中包含的IQueryProvider则要把查询操作转译成T-SQL,然后,T-SQL会交由远端的数据库引擎去执行,使得数据库引擎能够在它自己所处的环境中执行这些SQL语句(参见本章早前的第38条)。这样做的好处是可以在电脑之间少传输一些数据,有时也可以降低各层之间所需传输的数据量(但缺点则是程序有可能因为查询操作无法转译而崩溃)。

如果在性能与健壮这两项因素之间更看重后者,那么可以把查询结果明确转换成IEnumerable,这样做的缺点是LINQ to SQL引擎必须把dbContext.Products中的所有内容都从数据库中获取过来。此外还要注意,转换成IEnumerable之后,其余的查询操作就会放在本地来执行。由于IQueryable继承自IEnumerable,因此,包含这段代码的那个方法能够同时适用于这两种类型的序列。

这听上去还不错,而且写起来也很简单,但这种写法会迫使程序一律以IEnumerable的形式来处理调用方所传入的序列。即便传入的序列支持IQueryable,也必须先把所有数据都获取到本进程所在的地址空间,然后才能处理并返回。

一般情况下,应该把那些分别针对各个类型但又极为相似的逻辑纳入同一个方法中,该方法只需针对那个能与其他类型相兼容且最为具体的类或接口来编写即可。然而涉及IEnumerable与IQueryable的那些逻辑却不能像这样来统合。从表面上看,后者继承自前者,而且它们的功能又很接近,于是,似乎只需要针对IEnumerable编写出通用的版本就可以了。但实际上,由于两者的实现方式有所区别,因此,还应该针对每一种数据源分别编写一个版本才对。而且开发者也确实可以判断出来数据源究竟是只实现了IEnumerable,还是同时实现了IQueryable。若遇到后一种情况,则应设法将IQueryable的特性发挥出来。

有时,程序可能需要针对某种数据类型T来同时支持IEnumerable及IQueryable这两种形式的查询操作:

这两个方法之间有很多代码都是重复的。为此,开发者可以用AsQueryable()把IEnumerable试着转换成IQueryable,以便将这两个方法合并到一起:

AsQueryable()会判断序列的运行期类型。如果是IQueryable型,那就把该序列当成IQueryable返回。若是IEnumerable型,则会用LINQ to Objects的逻辑来创建一个实现IQueryable的wrapper(包装器),然后将其返回。尽管这个wrapper在其内部用的还是IEnumerable形式的处理逻辑,但它却是以IQueryable形式的引用而出现的。

使用AsQueryable()来编写代码可以同时顾及这两种情况,也就是说,无论序列是本身就已经实现了IQueryable,还是仅仅实现了IEnumerable,都能够充当数据源。如果是前一种情况,那么代码就可以适当地运用IQueryable中的方法了,而且还能够支持表达式树与远程执行等特性。如果是后一种情况,那么程序运行的时候,可以切换到针对IEnumerable的那套逻辑上面。

需要注意的是,现在的这个版本还是调用了一个方法,也就是string.LastIndex Of()方法,只不过,该方法恰好可以为LINQ to SQL库所解析,因此,能够放在LINQ to SQL查询中。然而由于各provider的能力不尽相同,因此,无法确保IQuery Provider的每一种实现方式都能够支持该方法。

IQueryable与IEnumerable这两种类型,在功能上似乎差不多,两者之间的区别,主要体现在实现查询模式时,所用的办法上面。如果要声明某个变量,用来保存查询结果,那么该变量的类型,一定要与数据源相匹配,由于查询方法是静态绑定的,因此,只有把变量的类型写对,程序才可以正常地运作。

来着《Effective C#:改善C#代码的50个有效方法(原书第3版)》

posted @ 2021-08-14 22:32  王哲66369  阅读(179)  评论(0编辑  收藏  举报