《深入理解C#》整理8-超越集合的LINQ
一、用IQueryable和IQueryProvider进行转换
在LINQ to SQL中的所有查询表达式中,数据源都是Table
1、IQueryable和相关接口的介绍
IQueryable
理解IQueryable的最简单方式就是,把它看作一个查询,在执行的时候,将会生成结果序列。从LINQ的角度看,由于是通过IQueryable的Expression属性返回结果,所以查询的详细信息就保存于表达式树中。一个查询进行执行,就是开始遍历IQueryable的过程(换句话说,即调用GetEnumerator方法,然后对其结果调用MoveNext方法),或者调用IQueryProvider上的Execute方法并传递表达式树。给定任何IQueryable查询后,你可通过执行如下步骤来创建新的查询:
(1) 请求现有查询的查询表达式树(使用Expression属性);
(2) 构建一个新的表达式树,包含最初的表达式和你想要的额外功能(例如,过滤、投影或排序);
(3) 请求现有查询的查询提供器(使用Provider属性);
(4) 调用提供器的CreateQuery方法,传递新表达式树。
2、模拟接口实现来记录调用
这里我们会编写IQueryable和IQueryProvider的实现,FakeQueryProvider和FakeQuery类型。实现如下:
无参数的构造函数,主程序用它为查询创建普通“数据源”;而FakeQueryProvider则会调用另一个构造函数并传入当前的查询表达式。Expression.Constant(this)用作初始数据源表达式只是为了展示查询表示原始的对象。(例如,假设某个实现表示一个数据表——如果不使用任何查询操作符,查询将返回整个数据表。)
上述CreateQuery方法不执行真正的处理,而是作为查询的工厂方法。Execute重载方法只是在记录调用日志后返回空结果。通常情况下,在这里应该完成大量的分析工作,以及对Web服务、数据库或任何目标平台的实际调用。
3、把表达式粘合在一起:Queryable的扩展方法
正如Enumerable类型包含着关于IEnumerable
①Enumerable的方法都使用委托作为参数,而对于Queryable来说则需要表达式树(Lambda表达式既可以被转换为委托实例,也可以转换为表达式树);
②Enumerable的扩展方法会完成与对应查询操作符相关的实际工作(至少会构建完成这些工作的迭代器)。而Queryable中的查询操作符的“实现”做的事情非常少:它们仅仅基于参数创建一个新的查询,或在查询提供器上调用Execute。换句话说,它们只用来构建查询和要执行的请求——不包含操作符背后的逻辑。这意味着,它们可用于任何使用表达式树的LINQ提供器,但是它们单独使用时没有任何意义。它们是代码和提供器细节之间的黏合剂。
4、模拟实际运行的查询提供器
GetEnumerator只在最后才调用,而不在任何中间查询中调用,并且在GetEnumerator被调用的时候,我们已经有了出现在原始查询表达式中的所有信息。到目前为止,单一的表达式树已经捕获了所有的信息。实际的表达式树可能非常深且复杂,特别是在Where子句包含额外方法调用的时候。LINQ to SQL检查表达式树以算出应该执行什么样的查询。当调用CreateQuery时,LINQ提供器能够构建它们自己的查询(以它们需要的任何形式),不过,当调用GetEnumerator的时候,看一下最后的表达式树,可知道它们通常都比较简单,这是因为所有需要的信息都已经保存在同一个地方了。
二、LINQ友好的API和LINQ to XML
1、LINQ to XML中的核心类型
LINQ to XML位于System.Xml.Linq程序集,并且大多数类型都位于System.Xml.Linq命名空间。下图展示了最常用的一些类型:
- XName表示元素和特性的名称。
- XNamespace表示XML命名空间,通常是一个URI。
- XObject是XNode和XAttribute的共同父类:与在DOM API中不同,在LINQ to XML中特性不是节点。如果某方法返回子节点的元素,这里面是不包含特性的
- XNode表示XML树中的节点。它定义了各种用于操作和查询树的成员。
- XAttribute表示包含名/值对的特性。
- XContainer是XML树中可以包含子内容(主要为元素或文档)的节点。
- XText表示文本节点,其派生类XCData表示CDATA文本节点。
- XElement表示元素。它和XAttribute是LINQ to XML中最常用的类。与在DOM API中不同,在创建一个XElement时,不需要创建包含它的文档。
- XDocument表示文档。可以通过Root属性访问其根元素,相当于XmlDocument.Document Element。
2、声明式构造
①在DOM API中,我们通常创建一个元素,然后向其中添加内容。在LINQ to XML中我们也可以这样做,使用继承自XContainer的Add方法,但这并不是LINQ to XML的惯用法。不过还是有必要看一下XContainer.Add的签名,因为它使用了内容模型。你也许会认为其签名为Add(XNode)或Add(XObject),但事实上它只是Add(object)。XElement(和XDocument)的构造函数签名也使用了同样的模式。在名称之后,你可以什么都不指定(创建空元素),也可以指定一个对象(创建包含单个子节点的元素),或对象数组(创建包含多个子节点的元素)。在创建多个子节点的时候,使用了参数数组(C#中的params关键字),这意味着编译器将为我们创建数组,我们只需要不断列出参数即可。
②在创建内容时,不管是通过构造函数还是Add方法,都要考虑以下几点:
- 空引用会被忽略
- XNode和XAttribute实例可以直接添加。如果它们已经有了父元素,将会被复制,但除此之外不需要任何转换
- 字符串、数字、日期、时间等将使用标准XML格式转换为XText
- 如果参数实现了IEnumerable,Add方法将迭代其内容,并添加各个值,必要的时候会使用递归
- 其他没有特殊处理的对象将调用ToString()将其转换为文本
3、查询单个节点
XElement包含很多轴方法,可用于查询资源,每个方法都返回适当的IEnumerable
4、合并查询操作符
查询的部分结果往往为另一个序列,而在LINQ to XML中则通常为元素的序列。如何找出各个项目中的某一个元素呢?这时我们需要对各个元素执行另一个查询,然后合并这些结果。LINQ to Objects已经提供了SelectMany操作符来实现该功能,但对于XML来说并非是理想的。LINQ to XML提供了一些扩展方法(位于System.Xml.Linq.Extensions类中),有的针对特殊的序列类型,有的是包含强制类型参数的泛型方法,以应对C# 4之前缺乏泛型接口协变性的问题。上节中提到的轴方法大多可作为扩展方法的形式使用。
因而下面的查询可以进行如下转变:
5、与LINQ和谐共处
LINQ to XML使用了如下三种方式与其他LINQ相适应:
- 在构造函数中消费序列。LINQ是刻意声明式语言,LINQ to XML支持声明式地创建XML结构。
- 在查询方法中返回序列。这大概是数据访问API必须遵循的最为明显的步骤:查询结果应该轻而易举地返回IEumerable
或实现了该接口的类。 - 扩展了可以对XML类型的序列所作的查询,这样可以让它们看上去更像是统一的查询API,尽管有些查询必须用于XML。
三、用并行LINQ代替LINQ to Objects
并行LINQ的背后理念是,某个LINQ to Objects查询需要执行很长的时间,而使用多线程利用多核优势进行查询则可以运行得很快,并且改动也很少。
1、在单线程中绘制曼德博罗特集
我们遍历每一行以及每行中的每一列,计算相关像素的索引。如下:
2、ParallelEnumerable、ParallelQuery和AsParallel
并行LINQ带来了一些新的类型,它们位于System.Linq命名空间,ParallelEnumerable是一个静态类,与Enumerable类似。它里面几乎全部是扩展方法,其中大多数都扩展了ParallelQuery这个类型。该类型包含泛型和非泛型形式(ParallelQuery
如何以并行查询开始呢?答案是调用AsParallel,它是ParallelEnumerable中的扩展方法,扩展了IEnumerable
3、调整并行查询
你只需要使用AsOrdered扩展方法,强制对查询排序即可。它比无序查询要略慢,但仍明显快于单线程版本
四、使用LINQ to Rx反转查询模型
当数据由消费者掌管,即当新数据可用的时候,由数据消费者进行响应,这就是所谓的“推”。有一个有趣的程序集叫做System.Interactive,它包含各种额外的LINQ to Objects方法;System.Reactive实现了各种推操作。
1、IObservable和IObserver
LINQ to Rx的数据模型与普通IEnumerable
这两个接口属于.NET 4(位于System命名空间),但LINQ to Rx的其余部分则是需要单独下载的。实际上,在.NET 4中这两个接口是IObservable
LINQ to Rx与我们熟悉的事件十分类似。调用一个可观察对象(observable)的Subscribe,就像是对事件使用+=来注册处理程序一样。Subscribe返回的可处置(disposable)值会记住传入的观察者(observer):处置它就像对同一个处理程序使用-=一样。通常,观察者将重复调用OnNext方法,并最终调用OnCompleted——这期间如果出现了某种错误,就用OnError代替。在序列结束或发生错误之后不会再调用其他方法。
2、简单的开始
这里我们使用Observable.Range来创建一个可观察的范围,而不再使用Enumerable.Range。每当一个观察者订阅这个范围时,使用OnNext把数字发送给该观察者,最后将调用OnCompleted。Range方法返回的是一个冷可观察对象(cold observable)。它处于休眠状态,直到某个观察者订阅了它,它才会向该观察者发送值。如果其他观察者也订阅了该对象,将会得到该范围的一个副本。这与点击按钮这种普通的事件不太相同,对于后者,多个观察者可以同时订阅同一个实际的值序列——并且即便没有任何观察者,也会有效地产生值。这种序列称为热可观察对象(hot observable)
3、查询可观察对象
相关示例如下:
我们在LINQ to Objects中处理分组时常常要嵌套foreach循环,因此在LINQ to Rx中要嵌套订阅。在进行分组操作,LINQ to Objects在返回之前把整个分组收集在一起,这意味着要对结果进行缓冲,直到序列的末尾。在某些情况下,在LINQ to Objects中所需的大量的数据缓冲操作,都可以用LINQ to Rx更高效地实现。
4、意义何在
Rx提供了一种优雅的方式来思考各种异步处理——如普通.NET事件(可以使用Observable. FromEvent将其视为可观察对象)、异步I/O和调用We b服务。它提供了一种有效的方式来管理复杂性和并发。
五、扩展LINQ to Objects
1、设计和实现指南
当为LINQ进行相关查询操作符的扩展时,应该注意以下几点:
- 单元测试:不要忘记测试个别情况,如空序列和无效参数等;
- 检查参数:好的方法会检查传入的参数。但这对LINQ操作符来说有一个问题。很多操作符都返回一个序列,而实现这种功能最简单的方式就是迭代器块。但你应该在调用方法的同时执行参数检查,而不应该等到调用者决定迭代其结果的时候。如果打算使用迭代器块,就把方法分成两部分:在公共方法中执行参数检查,然后调用一个私有方法进行迭代。
- 优化:IEnumerable
本身所支持的操作十分有限,但你所操作的序列的执行时类型可能具备更多的功能。 - 文档:在文档中指明代码对输入的处理和操作符的预期性能是十分重要的。
- 尽量只迭代一次:可以对IEnumerable
进行多次迭代——实际上对于同一个序列,你可以同时拥有多个活动的迭代器。但对于一个查询操作符来说,这样做可不是什么好主意。 - 释放迭代器:在大多数情况下,我们可以使用foreach语句来迭代数据源。在这种情况下,要为迭代器使用using块
- 自定义比较器:很多LINQ操作符都包含可以指定适当IEqualityComparer
或IComparer 的重载。通常简单的重载只需要调用复杂的重载,传入EqualityComparer .Default或Comparer .Default作为比较器