.NET深入解析LINQ框架(一:LINQ优雅的前奏)
阅读目录:1.LINQ简述2.LINQ优雅前奏的音符2.1.隐式类型 (由编辑器自动根据表达式推断出对象的最终类型)2.2.对象初始化器 (简化了对象的创建及初始化的过程)2.3.Lambda表达式 (对匿名方法的改进,加入了委托签名的类型推断并很好的与表达式树的结合)2.4.扩展方法 (允许在不修改类型的内部代码的情况下为类型添加独立的行为)2.5.匿名类型 (由对象初始化器推断得出的类型,该类型在编译后自动创建)2.6.表达式目录树(用数据结构表示程序逻辑代码)3.LINQ框架的主要设计模型3.1.链式设计模式(以流水线般的链接方式设计系统逻辑)3.2.链式查询方法(逐步加工查询表达式中的每一个工作点)4.LINQ框架的核心设计原理4.1.托管语言之上的语言(LINQ查询表达式)4.2.托管语言构造的基础(LINQ依附通用接口与查询操作符对应的方法对接)4.3.深入IEnumerable、IEnumerable<T>、Enumerable(LINQ to Object框架的入口)4.4.深入IQueryable、IQueryable<T>、Queryable(LINQ to Provider框架的入口)4.5.LINQ针对不同数据源的查询接口5.动态LINQ查询(动态构建Expression<T>表达式树)6.DLR动态语言运行时(基于CLR之上的动态语言运行时)1】.LINQ简述
LINQ简称语言集成查询,设计的目的是为了解决在.NET平台上进行统一的数据查询。
微软最初的设计目的是为了解决对象/关系映射的解决方案,通过简单的使用类似T-SQL的语法进行数据实体的查询和操作。不过好的东西最终都能良性的发展演化,变成了如今.NET平台上强大的统一数据源查询接口。[王清培版权所有,转载请给出署名]
我们可以使用LINQ查询内存中的对象(LINQ to Object)、数据库(LINQ to SQL)、XML文档(LINQ to XML),还有更多的自定义数据源。
使用LINQ查询自定义的数据源需要借助LINQ框架为我们提供的IQueryable、IQueryProvider两个重量级接口。后面的文章将讲解到,这里先了解一下。
在LINQ未出现之前,我们需要掌握很多针对不同数据源查询的接口技术,对于OBJECT集合我们需要进行重复而枯燥的循环迭代。对于数据库我们需要使用诸多T-SQL\PL-SQL之类的数据库查询语言。对于XML我们需要使用XMLDOM编程接口或者XPATH之类的东西,需要我们掌握的东西太多太多,即费力又容易忘。
那么LINQ是如何做到对不同的数据源进行统一的访问呢?它的优雅不是一天两天就修来的,归根到底还得感谢C#的设计师们,是他们让C#能如此完美的演变,最终造就LINQ的优雅。
下面我们来通过观察C#的每一次演化,到底在哪里造就了LINQ的优雅前奏。[王清培版权所有,转载请给出署名]
2】.LINQ优雅前奏的音符2.1.隐式类型(由编辑器自动根据表达式推断出对象的最终类型)隐式类型其实是编辑器玩的语法糖而已,但是它在很大程度上方便了我们编码。熟悉JS的朋友对隐式类型不会陌生,但是JS中的隐式类型与这里的C#隐式类型是有很大区别的。尽管在语法上是一样的都是通过var关键字进行定义,但是彼此最终的运行效果是截然不同。
JS是基于动态类型系统设计原理设计的,而C#是基于静态类型系统设计的,两者在设计原理上就不一样,到最后的运行时更不同。
这里顺便推荐一本C#方面比较深入的书籍《深入解析C#》,想深入学习C#的朋友可以看看。这书有两版,第二版是我们熟悉的姚琪琳大哥翻译的很不错。借此谢谢姚哥为我们翻译这么好的一本书。这本书很详细的讲解了C#的发展史,包括很多设计的历史渊源。来自大师的手笔,非常具有学习参考价值,不可多得的好书。
我们通过一个简短的小示例来快速的结束本小节。
2.4.扩展方法(允许在不修改类型的内部代码的情况下为类型添加独立的行为)
在图的第二行代码中,就是使用才有参数的方法调用GetModelList方法,无法进行真确的类型推断。
小结:按照这个分析,似乎对于方法的泛型类型推断只限于Lambda表达式?如果不是为什么多了参数就无法进行类型推断?我们先留着这个疑问等待答案吧;
扩展方法的本意在于不修改对象内部代码的情况下对对象进行添加行为。这种方便性大大提高了我们对程序的扩展性,虽这小小的扩展性在代码上来看不微不足道,但是如果使用巧妙的话将发挥很大的作用。扩展方法对LINQ的支撑非常重要,很多对象原本构建与.NET2.0的框架上,LINQ是.NET3.0的技术,如何在不影响原有的对象情况下对对象进行添加行为很有挑战。
那么我们利用扩展方法就可以无缝的嵌入到之前的对象内部。这样的需求在做框架设计时很常见,最为典型的是我们编写了一个.NET2.0版本的DLL文件作为客户端程序使用,那么我们有需要在服务端中对.NET2.0版本中的DLL对象加以控制。比如传统的WINFORM框架,我们可以将ORM实体作为窗体的控件数据源,让ORM实体与窗体的控件之间形成自然的映射,包括对赋值、设置值都很方便。但是这样的实体经过序列化后到达服务层,然后经过检查进入到BLL层接着进入到DAL层,这个时候ORM框架需要使用该实体作相应的数据库操作。那么我们如何使用.NET3.0的特性为ORM添加其他的行为呢?如果没有扩展方法这里就很无赖了。有了扩展方法我们可以将扩展方法构建与.NET3.0DLL中,在添加对.NET2.0DLL的友元引用,再对ORM实体进行扩展。[王清培版权所有,转载请给出署名]
我们来看一个小例子,看看扩展方法如果使用;
通过反射的方式我们就可以顺利的获取到匿名类型的属性成员,然后通过属性信息在顺利的获取到属性的值。[王清培版权所有,转载请给出署名]
2.6.表达式目录树(用数据结构表示逻辑代码)表达式目录树是LINQ中的重中之重,优雅其实就体现在这里。我们从匿名委托到Lambda拉姆达表达式在到现在的目录树,我们看到了.NET平台上的语言越来越强大。我们没有理由不去接受它的美。那么表达式目录树到底是啥东西,它的存在是为了解决什么样的问题又或者是为了什么需求而存在的?
我们上面已经讲解过关于Lambda表示式的概念,它是匿名函数的优雅编写方式。在Lambda表达式里面是关于程序逻辑的代码,这些代码经过编译器编译后就形成程序的运行时路径,根本无法作为数据结构在程序中进行操作。比如在Lambda表达式里面我编写了这样一段代码 :(Student Stu)=>Stu.Name=="王清培",那么这段代码经过编译器编译后就变成了大家耳熟能详的微软中间语言IL。那么在很多时候我们需要将它的运行特性表现为数据结果,我们需要人为的去解析它,并且转变为另外一种语言或者调用方式。那么为什么在程序里面需要这样的多此一举,不能用字符串的方式表达Lambda表达式等价的表达方式呢?这样的目的是为了保证强类型的操作,不会导致在编译时无法检查出的错误。而如果我们使用字符串的方式来表达逻辑的结构,那么我们只能在运行时才能知道它的正确性,这样的正确性是很脆弱的,不知道在什么样的情况下会出现问题。所以如果有了强类型的运行时检查我们就可以放心的使用Lambda这样的表达式,然后在需要的时候将它解析成各种各样的逻辑等式。
在.NET3.5框架的System.Linq.Expression命名空间中引入了以Expression抽象类为代表的一群用来表示表达式树的子对象集。这群对象集目的就是为了在运行时充分的表示逻辑表达式的数据含义,让我们可以很方便的获取和解析这中数据结构。为了让普通的Lambda表达式能被解析成Expression对象集数据结构,必须得借助Expression<T>泛型类型,该类型派生自LambdaExpression,它表示Lambda类型的表达式。通过将Delegate委托类型的对象作为Expression<T>中的类型形参,编辑器会自动的将Lambda表达式转换成Expression表达式目录树数据结构。我们看来例子;
不使用Expression<T>作为委托类型的包装的话,该类型将是普通的委托类型。
如果使用了Expression<T>作为委托类型的包装的话,编译器将把它解析成继承自System.Linq.Expression.LambdaExpression类型的对象。一旦变成对象,那么一切就好办多了,我们可以通过很简单的方式获取到Expression内部的数据结构。[王清培版权所有,转载请给出署名]
表达式目录树的对象模型;
上面简单的介绍了一下表达式目录树的用意和基本的原理,那么表达式目录树的继承关系或者说它的对象模型是什么样子的?我们只有理清了它的整体结构这样才能方便我们以后对它进行使用和扩展。
下面我们来分析一下它的内部结构。
(Student stu)=>stu.Name=="王清培",我定义了一个Lambda表达式,我们可以视它为一个整体的表达式。什么叫整体的表达式,就是说完全可以用一个表达式对象来表示它,这里就是我们的LambdaExpression对象。表达式目录树的本质是用对象来表达代码的逻辑结构,那么对于一个完整的Lambda表达式我们必须能够将它完全的拆开才能够进行分析,那么可以将Lambda表达式拆分成两部分,然后再分别对上一次拆开的两部分继续拆分,这样递归的拆下去就自然而然的形成一颗表达式目录树,其实也就是数据结构里面的树形结构。那么在C#里面我们很容易的构造出一个树形结构,而且这颗树充满着多态。
(Student stu)=>stu.Name="王清培",是一个什么样子的树形结构呢?我们来看一下它的运行时树形结构,然后在展开抽象的继承图看一下它是如何构造出来的。
上图中的第一个对象是Expression<T>泛型对象,通过跟踪信息可以看出,Expression<T>对象继承自LambdaExpression对象,而LambdaExpression对象又继承自Expression抽象类,而在抽象里重写了ToString方法,所以我们在看到的时候是ToString之后的字符串表示形式。
Lambda表达式对象主要有两部分组成,从左向右依次是参数和逻辑主题,也就对应着Parameters和Body两个公开属性。在Parameters是所有参数的自读列表,使用的是System.Collection.ObjectModel.ReadOnlyCollection<T>泛型对象来存储。
这里也许你已经参数疑问,貌似表达式目录树的构建真的很完美,每个细节都有指定的对象来表示。不错,在.NET3.5框架中引入了很多用来表示表达式树逻辑节点的对象。这些对象都是直接或间接的继承自Expression抽象类,该类表示抽象的表达式节点。我们都知道表达式节点各种各样,需要具体化后才能直接使用。所以在基类Expression中只有两个属性,一个是public ExpressionType NodeType { get; },表示当前表达式节点的类型,还有另外一个public Type Type { get; },表示当前表达式的静态类型。何为静态类型,就是说当没有变成表达式目录树的时候是什么类型,具体点讲也就是委托类型。因为在委托类型被Expression<T>泛型包装后,编译器是把它自动的编译成表达式树的数据结构类型,所以这里需要保存下当前节点的真实类型以备将来使用。[王清培版权所有,转载请给出署名]
小结:到了这里其实已经把LINQ的一些准备工作讲完了,从一系列的语法增强到.NET5.0的类库的添加,已经为后面的LINQ的到来铺好了道路。下面的几个小结将是最精彩的时刻,请不要错误哦。

