• 《深入理解C#》整理8-超越集合的LINQ


    一、用IQueryable和IQueryProvider进行转换

    在LINQ to SQL中的所有查询表达式中,数据源都是Table。不过,如果你看一下Table,你就会发现它没有Where、Select和Join方法,或任何其他的标准查询操作符。但是,它利用了和LINQ to Objects同样的技巧——LINQ to Objects中的数据源总是实现IEnumerable(可能在调用Cast或OfType之后) ,然后使用Enumerable中的扩展方法,而Table实现了IQueryable并使用Queryable的扩展方法。

    1、IQueryable和相关接口的介绍

    IQueryable从IEnumerable和非泛型的IQueryable继承而来,而IQueryable又继承于非泛型的IEnumerable。IQueryable仅有3个属性:Query Provider、ElementType和Expression。QueryProvider属性是IQueryProvider类型——另一个需要考虑的新接口

    image-20201028203820528

    理解IQueryable的最简单方式就是,把它看作一个查询,在执行的时候,将会生成结果序列。从LINQ的角度看,由于是通过IQueryable的Expression属性返回结果,所以查询的详细信息就保存于表达式树中。一个查询进行执行,就是开始遍历IQueryable的过程(换句话说,即调用GetEnumerator方法,然后对其结果调用MoveNext方法),或者调用IQueryProvider上的Execute方法并传递表达式树。给定任何IQueryable查询后,你可通过执行如下步骤来创建新的查询:

    (1) 请求现有查询的查询表达式树(使用Expression属性);

    (2) 构建一个新的表达式树,包含最初的表达式和你想要的额外功能(例如,过滤、投影或排序);

    (3) 请求现有查询的查询提供器(使用Provider属性);

    (4) 调用提供器的CreateQuery方法,传递新表达式树。

    2、模拟接口实现来记录调用

    这里我们会编写IQueryable和IQueryProvider的实现,FakeQueryProvider和FakeQuery类型。实现如下:

    image-20201028211643024

    无参数的构造函数,主程序用它为查询创建普通“数据源”;而FakeQueryProvider则会调用另一个构造函数并传入当前的查询表达式。Expression.Constant(this)用作初始数据源表达式只是为了展示查询表示原始的对象。(例如,假设某个实现表示一个数据表——如果不使用任何查询操作符,查询将返回整个数据表。)

    image-20201028211915742

    上述CreateQuery方法不执行真正的处理,而是作为查询的工厂方法。Execute重载方法只是在记录调用日志后返回空结果。通常情况下,在这里应该完成大量的分析工作,以及对Web服务、数据库或任何目标平台的实际调用。

    3、把表达式粘合在一起:Queryable的扩展方法

    正如Enumerable类型包含着关于IEnumerable的扩展方法来实现LINQ标准查询操作符一样,Queryable类型包含着关于IQueryable的扩展方法。IEnumerable和Queryable的实现之间有两个巨大的区别:

    ①Enumerable的方法都使用委托作为参数,而对于Queryable来说则需要表达式树(Lambda表达式既可以被转换为委托实例,也可以转换为表达式树);

    image-20201028212824152

    ②Enumerable的扩展方法会完成与对应查询操作符相关的实际工作(至少会构建完成这些工作的迭代器)。而Queryable中的查询操作符的“实现”做的事情非常少:它们仅仅基于参数创建一个新的查询,或在查询提供器上调用Execute。换句话说,它们只用来构建查询和要执行的请求——不包含操作符背后的逻辑。这意味着,它们可用于任何使用表达式树的LINQ提供器,但是它们单独使用时没有任何意义。它们是代码和提供器细节之间的黏合剂。

    4、模拟实际运行的查询提供器

    image-20201028213637539

    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命名空间。下图展示了最常用的一些类型:

    image-20201028214425626

    • 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()将其转换为文本

    image-20201028215926652

    3、查询单个节点

    XElement包含很多轴方法,可用于查询资源,每个方法都返回适当的IEnumerable,这意味着可以在查询出一些元素之后,使用普通的LINQ to Objects方法。除了这些返回序列的方法,有些方法还返回单个结果——其中最重要的是Attribute和Element,分别返回已命名的特性和具备指定名称的第一个子元素。

    image-20201028220630645

    4、合并查询操作符

    查询的部分结果往往为另一个序列,而在LINQ to XML中则通常为元素的序列。如何找出各个项目中的某一个元素呢?这时我们需要对各个元素执行另一个查询,然后合并这些结果。LINQ to Objects已经提供了SelectMany操作符来实现该功能,但对于XML来说并非是理想的。LINQ to XML提供了一些扩展方法(位于System.Xml.Linq.Extensions类中),有的针对特殊的序列类型,有的是包含强制类型参数的泛型方法,以应对C# 4之前缺乏泛型接口协变性的问题。上节中提到的轴方法大多可作为扩展方法的形式使用。

    因而下面的查询可以进行如下转变:

    image-20201028221648472

    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、在单线程中绘制曼德博罗特集

    我们遍历每一行以及每行中的每一列,计算相关像素的索引。如下:

    image-20201029192201651

    2、ParallelEnumerable、ParallelQuery和AsParallel

    并行LINQ带来了一些新的类型,它们位于System.Linq命名空间,ParallelEnumerable是一个静态类,与Enumerable类似。它里面几乎全部是扩展方法,其中大多数都扩展了ParallelQuery这个类型。该类型包含泛型和非泛型形式(ParallelQuery和ParallelQuery),我们大多数情况下都会使用其泛型形式,就像IEnumerable要比IEnumerable常用。此外,还有一个OrderedParallelQuery类,它是IOrderedEnumerable的并行版本。这些类型之间的关系如图所示:

    image-20201029192437882

    如何以并行查询开始呢?答案是调用AsParallel,它是ParallelEnumerable中的扩展方法,扩展了IEnumerable。该查询确实可以并行运行——但结果并不完全符合我们的要求:它的顺序与我们处理每行的顺序并不相同,为了改善性能,PLINQ默认使用无序查询。

    image-20201029192742012

    3、调整并行查询

    你只需要使用AsOrdered扩展方法,强制对查询排序即可。它比无序查询要略慢,但仍明显快于单线程版本

    image-20201029193112035

    四、使用LINQ to Rx反转查询模型

    当数据由消费者掌管,即当新数据可用的时候,由数据消费者进行响应,这就是所谓的“推”。有一个有趣的程序集叫做System.Interactive,它包含各种额外的LINQ to Objects方法;System.Reactive实现了各种推操作。

    1、IObservable和IObserver

    LINQ to Rx的数据模型与普通IEnumerable的模型在数学上是对偶的。它不向迭代器发出请求,而是提供一个观察者。然后,它也不请求下一个项,而是通知你的代码是否准备好了一个项、是否有错误发生、是否到达了数据末端。以下是涉及的两个接口的声明:

    image-20201029194054799

    这两个接口属于.NET 4(位于System命名空间),但LINQ to Rx的其余部分则是需要单独下载的。实际上,在.NET 4中这两个接口是IObservable和IObserver,分别表示IObservable是协变的,IObserver是逆变的。

    LINQ to Rx与我们熟悉的事件十分类似。调用一个可观察对象(observable)的Subscribe,就像是对事件使用+=来注册处理程序一样。Subscribe返回的可处置(disposable)值会记住传入的观察者(observer):处置它就像对同一个处理程序使用-=一样。通常,观察者将重复调用OnNext方法,并最终调用OnCompleted——这期间如果出现了某种错误,就用OnError代替。在序列结束或发生错误之后不会再调用其他方法。

    image-20201029194652999

    2、简单的开始

    这里我们使用Observable.Range来创建一个可观察的范围,而不再使用Enumerable.Range。每当一个观察者订阅这个范围时,使用OnNext把数字发送给该观察者,最后将调用OnCompleted。Range方法返回的是一个冷可观察对象(cold observable)。它处于休眠状态,直到某个观察者订阅了它,它才会向该观察者发送值。如果其他观察者也订阅了该对象,将会得到该范围的一个副本。这与点击按钮这种普通的事件不太相同,对于后者,多个观察者可以同时订阅同一个实际的值序列——并且即便没有任何观察者,也会有效地产生值。这种序列称为热可观察对象(hot observable)

    image-20201029195121128

    3、查询可观察对象

    相关示例如下:

    image-20201029195611777

    我们在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作为比较器

    2、示例扩展:选择随机元素

    image-20201029202038040

    作者:Jscroop
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    反射的基础详解
    数组,排序,枚举
    继承,多态,抽象,接口
    视图层 view
    常用类Object,String类详解
    模板层 Template
    自定义注解
    Django 高级
    常用类Math,StringBuffer,包装类,Date
    内部类,异常
  • 原文地址:https://www.cnblogs.com/Jscroop/p/13906643.html
Copyright © 2020-2023  润新知