日期:2014-05-20  浏览次数:20854 次

.NET深入解析LINQ框架(二:LINQ优雅的前奏)

3】.LINQ框架的主要设计模型

到了这里我们似乎隐隐约约的能看见LINQ的原理,它不是空中花园,它是有基础的。在上面的一系列新特性的支持下,微软通过大面积的构建扩展方法使得上述特性能连贯的互相作用,形成自然的集成查询框架。上面的这些特性都属于语言为了LINQ而做的增强,也可以说是设计者们在不断的探索新的比较符合现代开发体系的语言特性,也越来越多的支持函数式的编程特性,比如DLR的引入对Python、Ruby函数式脚本语言的强大支持,后面也会越来越多的支持其他的函数式脚本语言。

下面我们将主要学习对象模型的相关知识,什么是对象模型?其实很多时候我们注重的是语言层面的学习而并没有将重点放在对象的设计原理上,导致学习成本的不断增加。我们应该更重要的去学习和培养设计能力(所谓设计能力体现技术层次)。对象模型简单点讲就是对象的设计模型,如何构造能满足需要的深层对象结构。在目前.NET平台上的主流ORM框架ADO.NET EntityFramework中的架构体系中的概念层中的设计就体现出了对象模型的作用。在ADO.NET EntityFrameWork、Linq to SQL框架中有很多值得我们探索的对象模型。

在LINQ里面充斥着大量的扩展方法,在这些扩展方法的后背其实是隐藏着一个很大的设计秘密,那就是链式编程模型,下面我们将通过详细的学习链式编程模式来理解LINQ为什么能连贯的使用相同的方法而显现的如此优雅。[王清培版权所有,转载请给出署名]

  • 3.1.链式设计模式(以流水线般的链接方式设计系统逻辑)

链式设计模式是一直被我们忽视的一种很优美的模式,最近一次接触它的美是在学习LINQ的时候,看到连贯的扩展方法陆续登场顿时让我觉得这真是无可挑剔。其实在很多场合下我们也可以借鉴这种设计模式,可以很自然的处理很多比较棘手的问题。比较大胆的设计是业务碎片化后利用链式模式将碎片化后的业务算法进行人为的逻辑重组,如果设计的好的话,将是一道顶级盛宴。由于这篇文章是讲解LINQ的内容,这里我就不多扯它了,后面会有专门的文章来讲解大胆的链式业务流程重组的相关知识。

在很多时候我们设计一个系统功能或者应用框架时,完全可以借助链式设计模式来优雅我们的开发方式,使编码起来很顺利很方便。

为了很形象的表达链式设计模式的使用方式,这里我使用一个比较简单的小例子来展示它的设计理念和使用方式。

例子说明:假设我有一个表示学生的对象类型还有一个表示学生集合的类型。学生集合类型主要就是用来容纳学生实体,集合类型提供一系列的方法可以对这个集合进行连续的操作,很常用的就是筛选操作。比如筛选出所有性别是女生的学生,然后再在所有已经筛选出来的女性学生的集合当中筛选出年龄大于20周岁的学生列表,再继续筛选来自江苏南京地区的学生列表等等这一系列的连贯操作。这样的处理方式我想是LINQ最为常见的,毕竟LINQ是为了查询而生,而查询主要就是面向集合类的数据。[王清培版权所有,转载请给出署名]

对象图:

对象图中可以很清楚的看出各个对象中的属性和方法,在Student类中我们定义了几个基本的学生属性。而在StudentCollection中比较重要的是SelectByFemale方法和SelectByMankind方法,分别是筛选学生性别为女性和男性的方法,其他的就是SelectByAge和SelectByAddress分别是用来筛选年龄和地址的。由于具体的方法代码比较简单这里就不贴出来了,目的是为了让大家能直观的看出链式设计模式的好处和灵活的地方。

示例代码:

//构造Student数组
Student[] StudentArrary = new Student[3]
{
    new Student(){Name="王清培", Age=24, Sex="男", Address="江苏南京"},
    new Student(){Name="陈玉和", Age=23, Sex="女", Address="江苏盐城"},
    new Student(){Name="金源", Age=22, Sex="女", Address="江苏淮安"}
};
//使用Student数组初始化StudentCollection集合
StudentCollection StudentCollection = new StudentCollection(StudentArrary);
StudentCollection WhereCollection =
    StudentCollection.SelectByFemale().//筛选出所有女性学生列表
    SelectByAge(20).//筛选出年龄在20岁的学生列表
    SelectByAddress("江苏南京");//筛选出地址为“江苏南京”的学生列表

看起来是不是很优雅,我反正觉得很优雅很舒服。其实在我们设计StudentCollection对象内部方法的时候可能有一个地方很别扭,那就是方法的每次返回类型必须能让下一次的方法调用顺利进行,所以必须保持每次方法的调用都是同一种数据类型,也就是StudentCollection集合类型。

很多时候我们的设计思维存在着盲点,那就是每次返回后和本次没关系,链式编程似乎找到了这个盲点并且很严肃的跟我们强调要经常性的去锻炼这个设计盲点。我们利用思维导图来分析一下链式设计的盲点在哪里,也顺便来找找我们经常忽视的设计优点。

思维导图:

 

上图中每个方法都具有返回返回类型,但是只要保证返回的类型能是下一个方法的操作对象就行了,在设计对象方法的时候肯定是需要将大的过程拆分成一个可以组织的小过程。很多时候我们设计对象模型的时候也很难想到这些,其实也是我们不够熟练罢了,我们要做的就是多练习多看设计类的书,其他的交给时间吧。[王清培版权所有,转载请给出署名]

  • 3.2.链式查询方法(逐步加工查询表达式中的每一个工作点)

在上面的链式设计模式中我们大概了解到如果构建一个形成环路的对象模型,这样就可以反复的使用对象集合来执行重复的查询操作。其实LINQ就是使用这种方式来作为它的查询原理的。这里将直接点题到LINQ的核心设计原理上。LINQ的链式模型主要用在了查询对象集合上,通过大面积构建扩展方法让对象充满可以使用的LINQ表达式所对应的查询方法。

那么我们如何来理解LINQ的查询呢?大部分的同志都知道LINQ的语法,都是"from *** in *** where ***  select *** " 类似SQL这样的语法。其实这是构建与CTS之上的一种由编辑器负责处理的新的查询语法,它不是C#也不是VB.NET之类的托管语言。其实我们都知道C#、VB.NET之类的语法都是基于.NET平台的IL中间语言,他们属于源代码的一部分,并不是程序的最终输出项。而IL才是我们每次编译之后的输出项的程序代码。LINQ的语法最终也是IL的语法,当我们编写LINQ的查询表达式的时候其实编辑器已经智能的帮我们翻译成对象的方法。太多的原理在下一结介绍。

关于链式查询方法也是一个对象设计问题,我们参见链式设计模式可以很自然的构建符合我们自己实际需求的链式查询方法,这一系列的查询方法的添加存在一个很大的问题就是无法动态的添加到要扩展的对象内部去。比如对已经发布的对象是无法进行直接修改的,所以这里就用到了我们上面提到的扩展方法技术,通过扩展方法我们很方便的为已经发布的对象添加行为。为了具有说服力我们还是看一个小列子来加强印象。[王清培版权所有,转载请给出署名]

例子说明:假设我有一套已经发布的ORM简易型的组件,这个组件构建于.NET2.0之上,现在我需要将它扩展成链式的查询方式,而不想再使用以前繁琐的查询方式。所以我需要单独建立一个.NET3.0或.NET3.5的扩展作为以前程序集的一个扩展程序集,在使用的时候可以使用或者可以不使用,只有这样我们才能使用扩展方法或者其他的新的语法特性。

/// <summary>
        /// 根据 Base_Deptment 对象中的已有属性获取 Base_Deptment 对象集合。
        /// </summary>
        /// <param name="model">Base_Deptment 实例</param>
        /// <returns>Base_Deptment 对象实例集合</returns>
        [ContextLogHandler(OperationSort = 1, OperationExtension = typeof(Dal.LogWrite))]
        public List<Base_Deptment> GetAllListByPropertyValue(Base_Deptment model)
        {
            return ORMHelper.FindEntityList<Base_Deptment>(model);
        }

ORMHelper.FindEntityList<T> 是一段根据实体现有属性查询对象列表的泛型方法,当然这里是为了演示就比较简单点。如果我需要添加其他的条件就必须为Base_Deptment类型参数 model添加值才能使用,现在我想通过链式设计模式扩展它成为链式查询的使用方式,如:

/// <summary>
        /// 根据 Base_Deptment 对象中的已有属性获取 Base_Deptment 对象集合。
        /// </summary>
        /// <param name="model">Base_Deptment 实例</param>
        /// <returns>Base_Deptment 对象实例集合</returns>
        [ContextLogHandler(OperationSort = 1, OperationExtension = typeof(Dal.LogWrite))]
        public List<Base_Deptment> GetAllListByPropertyValue(Base_Deptment model)
        {
            Base_Deptment QueryModel = new Base_Deptment();
            var selectList = QueryModel.Select((Base_Deptment Model) => Model.FiledObject.BDeptCode)
            .Where((Model) => Model.FiledObject.BDeptCode == "800103848")
            .OrderByAscending((Model) => model.FiledObject.BDeptCreateTime);
            return selectList;
        }

这里的代码只是配合上下文理解,可能有些不太合理的地方,但是没有什么影响。

这样就可以将一个原本很臃肿的功能设计成如此优雅的使用方式。对于Linq to CustomEntity 实现我后面会有专门的文章讲解,这里也就不往下扯了。例子本身是想告诉我们可以借鉴链式查询实现更为人性化、优雅的组件或者框架。[王清培版权所有,转载请给出署名]

4】.LINQ框架的核心设计原理

  • 4.1.托管语言之上的语言(LINQ查询表达式)

通过上面的例子我们应该基本了解了链式设计模式、链式查询方法的奥妙和用武之地。通过一个简单的例子我们也认识到链式查询方法在数据查询方面具有独特的优势,这恰恰也是理解LINQ的好思路。

那么链式查询方法为LINQ准备了些什么?准备了对应的方法?没错,链式设计模式为链式查询做好了充足的理论基础,然后通过大面积的构建链式查询方法与LINQ查询表达式的查询操作符做对应自然就行成了使用LINQ查询任何数据源的好纽带。LINQ提供统一的查询接口,然后通过自定义的链式查询方法将用户的操作数据形成Lambda表达式,再通过提取Lambda表达式中的相关数据结构组织成你自己想要的参数送往数据驱动程序查询数据。

LINQ本身不属于托管语言的范畴,它是编辑器支撑的一种方便性的语法,目的是减少我们直接使用查询方法的麻烦。相比之下,如果我们直接使用查询方法那么所付出的精力和时间将会很多。

示例代码:

//构造Student数组
Student[] StudentArrary = new Student[3]
{
    new Student(){Name="王清培", Age=24, Sex="男", Address="江苏南京"},
    new Student(){Name="陈玉和", Age=23, Sex="女", Address="江苏盐城"},
    new Student(){Name="金源", Age=22, Sex="女", Address="江苏淮安"}
};

var list = StudentArrary.Where((studentModel) => studentModel.Name == "王清培").Select((studentModel) => studentModel);
var list = from i in StudentArrary where i.Name == "王清培" select i;

有两种方式查询集合数据,第一种是使用链式查询方式查询数据。第二种是使用LINQ查询表达式查询数据。毋庸置疑肯定是LINQ方便,简单方便更符合我们习惯的SQL查询方式。

这样我们就可以很轻松的得出一个筛选过后的对象。编辑器负责对LINQ进行处理而不是CLR负责对LINQ进行处理,编辑器将LINQ处理成框架所实现的基本接口集。记住,LINQ是语法糖层面的,它不是C#不是VB.NET更不是CLR的基本内核的支持。[王清培版权所有,转载请给出署名]

  • 4.2.托管语言构造的基础(LINQ依附通用接口与查询操作符对应的方法对接)

LINQ是统一的数据查询接口,那么它如何做到与不同的数据源直接衔接的?在4.1小结中,我们通过一个简单的LINQ查询表达式很方便的查询出了Student[]数组中的指定项,这里面是如何工作的?下面我们就来一步一步分析LINQ如何做到统一数据查询的。

我们现在假设没有LINQ,看看.NET是如何一点一点构建支持LINQ的内库的。

LINQ是在.NET3.5版本中引入的,核心程序集也就是System.Core.dll,有两个命名空间是直接关系到LINQ的,分别是System.Linq(LINQ查询表达式直接对应的链式查询方法集)、System.Linq.Expressions(LINQ查询表达式中的逻辑表达式树)。在System.Linq中首要的就是Enumerable静态类,该类是封装了对查询IEnumerable接口类型的静态扩展方法。这里需要注意的是,LINQ查询的数据源主要分为两类,必须支持的也是首先要支持的就是Linq to object,对于内存中的对象查询当然是以IEnumerable对象为主,查询是面向集合类的,在.NET里面是使用IEnumerable作为迭代器对象的实现接口,所以在System.Linq.Enumerable静态类中全部是封装了对IEnumerable接口的链式查询方法,这些方法都是通过扩展方法提供的,也就是在.NET3.5以下的版本中是没有的,扩展程序集包是不会被加载的。更为关键的是所有的扩展方法中的逻辑表达式都是Func泛型委托,也就是直接使用委托去执行逻辑操作,在我们调用的时候是以Lambda的形式给出逻辑的条件,这些逻辑被直接编译成可以执行的匿名方法,而不是表达式对象Expression,对于内存中的对象查询直接调用就行了。

另外一类LINQ支持的查询对象便是我们自定的数据源了,这类数据源的查询链式方法是由System.Linq.Queryable类提供的,如果我们使用LINQ查询表达式来查询System.Linq.IQueryable<T>类型对象的话,编辑器会认为你是查询自定的数据源对象,在执行的时候会调用你实现的System.Linq.IQueryableProvider<T>接口实现类。该类提供对表达式树的解析和执行。细看System.Linq.Queryable静态类中的所有扩展方法与System.Linq.Enumerable类中的扩展方法的区别便是所有的Func类型都被System.Linq.Expressions.Expression<T>类型包装着,这也符合我们上篇文章所讲的,对System.Linq.Expressions.Expression的解析是当成数据结构的,在需要的时候我们自己来读取相关的逻辑结构。

不管是查询Linq to object 还是自定的数据源,查询的LINQ语法是不变的,这也就是统一了数据查询接口,要变的是数据查询提供程序,Linq to Sql、Linq to Entities都是实现了自定义的数据源查询功能。[王清培版权所有,转载请给出署名]

 

  • 4.3.深入IEnumerable、IEnumerable<T>、Enumerable(LINQ to Object框架的入口)

在4.2结中已经为LINQ的查询做了支撑,那么查询到底区别在什么地方?在使用IEnumerable<T>和IQueryable<T>之间的区别是什么?如何很好的理解这两者在LINQ的整个框架中的关系。

LINQ是统一了.NET平台上的数据查询接口,不管我们想查询什么类型的数据,也不管这个数据在网络世界的何方,我们都可以很好的查询到。那么也不管我们想查询什么样的数据都需要我们创建成熟的对象模型才行,如果还是直接的将数据从服务器拖下来然后还是一个DataTable或者是一个DOM树,其实是意义不大的,我们需要的是能连续的在内存中对对象进行查询。当我们把数据从远程服务器中查询到内存中后需要使用我们创建的对象模型对象化它,为Linq to object做准备。只有Linq to object可以了Linq to custom才可以完美的执行,这是个反向关系。

泛型的IEnumerable<T>接口继承自IEnumerable接口,该接口表示可迭代的数据集合。Linq to object 也就是查询IEnumerable<T>集合。Enumerable静态类中的所有静态方法都是对应着操作IEnumerable<T>集合类型的LINQ查询表达式的,当每次查询时都是直接的调用Enumerable里面的静态方法。对于Linq to object 其实没有太多好讲的了,要做的就是熟悉LINQ的查询表达式语法。

  • 4.4.深入IQueryable、IQueryable<T>、Queryable(LINQ to Provider框架的入口)

IQueryable接口是提供给我们来实现自定义数据源用的,为了支持强类型的数据源集合我们直接使用IQueryable<T>接口,当我们使用LINQ来查询IQueryable<T>接口时查询表达式会被直接编译成对应的Queryable静态类中的对应的静态扩展方法。逻辑条件这个时候是被当成查询表达式处理的,而不像IEnumerable<T>接口直接是委托。当然,要想自己实现LINQ查询数据源还是比较难的,我们需要自行的去处理表达式目录树才行,后面的文章将会详细的讲解到。[王清培版权所有,转载请给出署名]

  • 4.5.LINQ针对不同数据源的查询接口

到目前为止我想我们都对LINQ的统一数据源查询有了大致的了解,不管我们的数据源是什么,RDMS、DOM等等,我们都有相对应的查询方法,辛苦的只是封装的人而已,做后台开发的朋友可能需要借助这些专门的查询语言来查询数据,给前端程序员方便的使用LINQ查询数据源。

组件开发人员首要的任务就是创建对象模型,该对象模型应该是真正数据源的抽象模型,以便于该对象可能成功的被放入到IQueryable<T>中进行查询。

  • 4.6.整体梳理LINQ的框架原理

通过上面的详细的介绍我们对LINQ的框架基本掌握了,如果只是使用它其实是很简单的,只要熟悉LINQ的查询语法就行了,但是我想我们每个程序员都有很强的好奇心,想搞懂框架的设计原理,这也是我们必须具备的战斗力。

LINQ查询表达式最后是调用的链式查询方法,这些方法都是在静态类中定义好的,IEnumerable<T>类型是直接的使用匿名方法调用执行,而IQueryable<T>是使用人工解析的方式进行的,也就是自定义数据源。Linq to xml、Linq to sql、Linq to Entities等等还有一些轻量级的查询库都是很优秀的扩展数据源例子,很值得我们去挖掘学习。[王清培版权所有,转载请给出署名]