- 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-SQLPL-SQL之类的数据库查询语言。对于XML我们需要使用XMLDOM编程接口或者XPATH之类的东西,需要我们掌握的东西太多太多,即费力又容易忘。
那么LINQ是如何做到对不同的数据源进行统一的访问呢?它的优雅不是一天两天就修来的,归根到底还得感谢C#的设计师们,是他们让C#能如此完美的演变,最终造就LINQ的优雅。
下面我们来通过观察C#的每一次演化,到底在哪里造就了LINQ的优雅前奏。[王清培版权所有,转载请给出署名]
2】.LINQ优雅前奏的音符
-
2.1.隐式类型(由编辑器自动根据表达式推断出对象的最终类型)
隐式类型其实是编辑器玩的语法糖而已,但是它在很大程度上方便了我们编码。熟悉JS的朋友对隐式类型不会陌生,但是JS中的隐式类型与这里的C#隐式类型是有很大区别的。尽管在语法上是一样的都是通过var关键字进行定义,但是彼此最终的运行效果是截然不同。
JS是基于动态类型系统设计原理设计的,而C#是基于静态类型系统设计的,两者在设计原理上就不一样,到最后的运行时更不同。
这里顺便推荐一本C#方面比较深入的书籍《深入解析C#》,想深入学习C#的朋友可以看看。这书有两版,第二版是我们熟悉的姚琪琳大哥翻译的很不错。借此谢谢姚哥为我们翻译这么好的一本书。这本书很详细的讲解了C#的发展史,包括很多设计的历史渊源。来自大师的手笔,非常具有学习参考价值,不可多得的好书。
我们通过一个简短的小示例来快速的结束本小节。
- List<Order> OrderList = new List<Order>()
- {
- new Order(){ Count=1},
- new Order(){ Count=2},
- new Order(){ Count=3}
- };
- foreach (Order order in OrderList)
- {
- Console.WriteLine(order.Count);
- }
这里我定义了一个List<Order>对象并且初始化了几个值,然后通过foreach迭代数据子项。其实这种写法很正常,也很容易理解。但是从C#3起加入了var关键字,编辑器对var关键字进行了自动分析类型的支持,请看下面代码。[王清培版权所有,转载请给出署名]
- var OrderList = new List<Order>()
- {
- new Order(){ Count=1},
- new Order(){ Count=2},
- new Order(){ Count=3}
- };
- foreach (var order in OrderList)
- {
- Console.WriteLine(order.Count);
- }
编辑器可以智能的分析出我们定义是什么类型,换句话说在很多时候我们确实需要编辑器帮我们在编译时确定对象类型。这在LINQ中很常见,在你编写LINQ查询表达式时,你人为的去判断对象要返回的类型是很不现实的,但是由编译器来自动的根据语法规则进行分析就很理想化了。由于LINQ依赖于扩展方法,进行链式查询,所以类型在编写时是无法确定的。后面的文章将详细的讲解到,这里先了解一下。
-
2.2.对象初始化器(简化了对象的创建及初始化的过程)
其实对象初始化器是一个简单的语法改进,目的还是为了方便我们进行对象的构造。(所谓万事俱备只欠东风,这个东风就是LINQ的方案。所以必须得先万事俱备才行。)
那么对象初始化器到底有没有多大的用处?我们还是先来目睹一下它的语法到底如何。
注意:对象初始化器只能用在属性、公共字段上。
- var order = new Order() { Count = 10, OrderId = "123", OrderName = "采购单" };//属性初始化
- var OrderList = new List<Order>()
- {
- new Order(){ Count=1, OrderId="1",OrderName="采购单"},
- new Order(){ Count=2, OrderId="2",OrderName="采购单"},
- new Order(){ Count=3, OrderId="3",OrderName="采购单"}
- };//集合初始化
属性初始化用这种语法编写的效果和直接用(order.Count=10;order.OrderId="123";order.OrderName="采购单";)是相等的。
集合初始化使用大括号的多行语法也很容易理解。类不具体的子对象的数据赋值是相同的。
我想对代码有追求的朋友都会很喜欢这种语法,确实很优美。[王清培版权所有,转载请给出署名]
-
2.3.Lambda表达式(对匿名方法的改进,加入了委托签名的类型推断并很好的与表达式树的结合)
我想没有朋友对Lambda表达式陌生的,如果你对Lambda表达式陌生的也没关系,这里照看不误。后面再去补习一下就行了。
在LINQ的查询表达式中,到处都是Lambda造就的优雅。通过封装匿名方法来达到强类型的链式查询。
Lambda是函数式编程语言中的特性,将函数很简单的表示起来。不仅在使用时方便,查找定义也很方便。在需要的时候很简单定义就可以使用了,避免了在使用委托前先定义一个方法的繁琐。Lambda表达式与匿名委托在语法上是有区别的,当然这两者都是对匿名函数的封装。但是他们的出现是匿名委托早于Lambda。所以看上去还是Lambda显得优雅。
下面我们来看一个小示例,简单的了解一下Lambda的使用原理,最重要的是它优于匿名委托哪里?
- /// <summary>
- /// 按照指定的逻辑过滤数据
- /// </summary>
- public static IEnumerable<T> Filter<T>(IEnumerable<T> ObjectList, Func<T, bool> FilterFunc)
- {
- List<T> ResultList = new List<T>();
- foreach (var item in ObjectList)
- {
- if (FilterFunc(item))
- ResultList.Add(item);
- }
- return ResultList;
- }
我们定义一个用来过滤数据的通用方法,这是个泛型方法,在使用时需要指定类型实参。方法有两个参数,第一个是要过滤的数据集合,第二个是要进行过滤的逻辑规则封装。
我们看一下调用的代码:
我们这里定义的逻辑规则是,只要大于3的我就把提取出来并且返回。很明显这里的(int item) => { return item > 3; }语法段就是Lambda表达式,它很方便的封装了方法的逻辑。从这点上看Lambda明显要比匿名委托强大很多,最重要的是它还支持泛型的类型推断特性。[王清培版权所有,转载请给出署名]
- int[] Number = new int[5] { 1, 2, 3, 4, 5 };
- IEnumerable<int> result = Filter<int>(Number, (int item) => { return item > 3; });
- foreach (var item in result)
- {
- Console.WriteLine(item);
- }
那么什么是泛型的类型推断?
其实泛型的类型推断说简单点就是类型实参不需要我们显示的指定,编辑器可以通过分析表达式中的潜在关系自动的得出类型实参的类型。
说的有点空洞,我们还是看具体的代码比较清晰。
- int[] Number = new int[5] { 1, 2, 3, 4, 5 };
- var result = Filter(Number, (int item) => { return item > 3; });
我将上面的代码修改成了不需要显示指定泛型类型实参调用,这里也是可以的。
我们在定义Filter<T>泛型方法时将Func<T,bool>泛型委托中的T定义为匿名函数的参数类型,所以在我们使用的时候需要指定出类型实参(int item)中的item来表示委托将要使用的类型参数形参。在编辑器看来我们在定义泛型方法Filter时所用的泛型占位符T也恰巧是Filter方法的形参数据类型Func<T,bool>中使用的调用参数类型,所以这里的语法分析规则能准确的推断出我们使用的同一种泛型类型实参。(这里要记住目前IDE编辑器只支持方法调用的泛型类型推断,也就是说其他方面的泛型使用是不支持隐式的类型推断,还是需要我们手动加上类型实参。)
这里顺便提一下关于延迟加载技术,延迟加载技术在集合类遍历非常有用,尤其是在LINQ中。很多时候我们对集合的处理不是实时的,也就是说我获取集合的数据不是一次性的,需要在我需要具体的某一个项的时候才让我去处理关于获取的代码。我稍微的改动了一下Filter代码:
- /// <summary>
- /// 按照指定的逻辑过滤数据。具有延迟加载的特性。
- /// </summary>
- public static IEnumerable<T> FilterByYield<T>(IEnumerable<T> ObjectList, Func<T, bool> FilterFunc)
- {
- foreach (var item in ObjectList)
- {
- if (FilterFunc(item))
- yield return item;
- }
- }
这里使用了yield关键字,使用它我们可以在方法内部形成一个自动的状态机结构。简单点讲也就是说系统会帮我们自动的实现一个继承了IEnumerable<T>接口的对象,在之前我们需要自己去实现迭代器接口成员,很费时费力而且性能不好。用这种方式定义的方法后,我们只有在遍历具体的集合时方法才会被调用,也算是一个很大的性能提升。[王清培版权所有,转载请给出署名]
泛型类型推断的不足之处;
当然类型推断还存在不足的地方,这里可以顺便参见一下我们老赵大哥的一篇文章:“C#编译器对泛型方法调用作类型推断的奇怪问题”;我在实际工作中也遇到过一个很头疼问题,这里顺便跟大家分享一下。按照常理说我在泛型方法的形参里面定义一个泛型的委托,他们的形参类型都是一样的占位符,但是如果我使用带有形参的方法作为委托的参数的话是无法进行类型推断的,然后使用无参数的方法作为委托参数是完全没有问题的。然后必须使用Lambda表达式才能做正确的类型推断,如果直接将带有参数的某个方法作为委托的参数进行传递是无法进行真确的类型推断,这里我表示很不理解。贴出代码与大家讨论一下这个问题。
我定义两个方法,这两个方法没有什么意义,只是一个有参数,一个没有参数。
无参数的方法:
- public static List<Order> GetOrderList()
- {
- return new List<Order>();
- }
有参数方法:
- public static List<Order> GetOrderListByModel(Order model)
- {
- return new List<Order>();
- }
Order对象只是一个类型,这里没有什么特别意义。
两个带有Func委托的方法,用来演示泛型的类型推断:
- public static TResult GetModelList<TResult>(Func<TResult> GetFunc)
- {
- return default(TResult);
- }
- public static TResult GetModelList<TSource, TResult>(Func<TSource, TResult> GetFunc)
- {
- return default(TResult);
- }
这里的问题是,如果我使用GetOrderList方法作为GetModelList<TResult>(Func<TResult> GetFunc)泛型方法的参数是没有任何问题的,编辑器能真确的推断出泛型的类型。但是如果我使用GetOrderListByModel作为GetModelList<TSource, TResult>(Func<TSource, TResult> GetFunc)重载版本的泛型方法时就不能真确的推断出类型。其实这里的Func中的TResult已经是方法的返回类型,TSource也是方法的参数类型,按道理是完全可以进行类型推断的。可是我尝试了很多种方式就是过不起。奇怪的是如果我使用带有参数和返回类型的Lambda表达式作为GetModelList<TSource, TResult>(Func<TSource, TResult> GetFunc)方法的参数时就能正确的类型推断。 [王清培版权所有,转载请给出署名]
方法调用的图例:
在图的第二行代码中,就是使用才有参数的方法调用GetModelList方法,无法进行真确的类型推断。
小结:按照这个分析,似乎对于方法的泛型类型推断只限于Lambda表达式?如果不是为什么多了参数就无法进行类型推断?我们先留着这个疑问等待答案吧;
-
2.4.扩展方法(允许在不修改类型的内部代码的情况下为类型添加独立的行为)
扩展方法的本意在于不修改对象内部代码的情况下对对象进行添加行为。这种方便性大大提高了我们对程序的扩展性,虽这小小的扩展性在代码上来看不微不足道,但是如果使用巧妙的话将发挥很大的作用。扩展方法对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实体进行扩展。[王清培版权所有,转载请给出署名]
我们来看一个小例子,看看扩展方法如果使用;
- public class OrderCollection
- {
- public List<Order> list = new List<Order>();
- }
- public class Order
- {
- public int Count;
- public string OrderName;
- public string OrderId;
- }
这里仅仅是为了演示,比较简单。我定义了一个Order类和一个OrderCollection类,目前看来OrderCollection没有任何的方法,下面我们通过添加一个扩展方法来为OrderCollection类添加一写计算方法,比如汇总、求和之类的。
如何定义扩展方法?
扩展方法必须是静态类中的静态方法,我们定义一个OrderCollection类的扩展方法Count。
- public static class OrderExtend
- {
- public static int Count(this OrderCollection OrderCollectionObject)
- {
- return OrderCollectionObject.list.Count;
- }
- }
扩展方法的第一个参数必须是this 关键开头然后经跟要扩展的对象类型,然后是扩展对象在运行时的实例对象引用。如果没有实例对象的引用我想扩展方法也毫无意识。所以这里我们使用Count方法来汇总一共有多少Order对象。通过OrderCollectionObject对象引用我们就可以拿到实例化的OrderCollection对象。
- OrderCollection orderCollection = new OrderCollection();
- orderCollection.Count();
还有一个需要大家注意的是,如果我们定义的扩展方法在另外的命名空间里,我们在使用的时候一定要在当前的CS代码中应用扩展方法所在的命名空间,要不然编辑器是不会去寻找你目前在使用的对象的扩展方法的,切忌。这里还有一点是需要我们注意的,当我们在设计后期可能会被扩展方法使用的对象时需要谨慎的考虑对象成员访问权限,如果我们将以后可能会被扩展方法使用的对象设计成受保护的或者私有的,那么可能会涉及到无法最大力度的控制。[王清培版权所有,转载请给出署名]
-
2.5.匿名类型(由对象初始化器推断得出的类型,该类型在编译后自动创建)
匿名类型其实也是比较好理解的,顾名思义匿名类型是没有类型定义的类型。这种类型是由编辑器自动生成的,仅限于当前上下文使用。废话少说了,我们还是看例子吧;
- var Student1 = new { Name = "王清培", Age = 24, Sex = "男", Address = "江苏淮安" };
- var Student2 = new { Name = "陈玉和", Age = 23, Sex = "女", Address = "江苏盐城" };
定义匿名类型跟普通的定义类型差不多,只不过在new之后是一对大括号,然后经跟着你需要使用到的属性名称和值。
匿名类型的作用域;
匿名类型在使用上是有它先天性缺点的,由于缺乏显示的类型定义,所以无法在方法之间传递匿名类型。要想获取匿名类型的各属性值只能通过反射的方式动态的获取运行时的属性对象,然后通过属性对象去获取到属性的值。匿名类型在使用的时候才会被创建类型,所以它在运行时存在着完整的对象定义元数据,所以通过反射获取数据是完全可以理解的。
下面我们使用上面定义的类型来获取它的各个属性。
- PrintObjectProperty(Student1, Student2);
- public static void PrintObjectProperty(params object[] varobject)
- {
- foreach (object obj in varobject)
- {
- foreach (System.Reflection.PropertyInfo property in obj.GetType().GetProperties())
- {
- Console.WriteLine(string.Format("PropertyName:{0},PropertyValue:{1}",
- property.Name, property.GetValue(obj, null)));
- }
- }
- }
图例:
通过反射的方式我们就可以顺利的获取到匿名类型的属性成员,然后通过属性信息在顺利的获取到属性的值。[王清培版权所有,转载请给出署名]
-
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表达式目录树数据结构。我们看来例子;
- Func<int> Func = () => 10;
- Expression<Func<int>> Expression = () => 10;
编辑器对上述两行代码各采用了不同的处理方式,请看跟踪对象状态。
不使用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>泛型包装后,编译器是把它自动的编译成表达式树的数据结构类型,所以这里需要保存下当前节点的真实类型以备将来使用。
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等等还有一些轻量级的查询库都是很优秀的扩展数据源例子,很值得我们去挖掘学习。
5】.动态LINQ查询(动态构建Expression<T>表达式树)
什么是动态LINQ查询?LINQ的编写是静态的,因为C#是基于静态类型系统原理设计的,在编写时已经确定类型,也就是在编译时就已经知道将要执行什么样的查询,条件是什么、排序方式是什么等等。那么很大一部分应用场合中我们需要根据用户的选择来查询数据源,以往我们都是通过判断的方式来拼接查询的SQL字符串,但是现在我们面对是强类型的LINQ查询,是否可以很方便的进行类似查询。其实也没有什么好神秘的,基本的实现原理是通过动态的构建表达式树来实现IQueryable<T>接口的查询。
其实动态LINQ查询所能执行的最关键的因素在于Expression<T>对象是可以被动态编译成可以执行的委托对象,委托对象是完全可以被直接使用的可执行代码段,这就为动态LINQ查询提供了基础。对于IEnumerable<T>类型的查询表达式方法都知道它的执行是不会直接接受Expression<T>类型对象的,那么动态LINQ是否能工作于IEnumerable<T>接口?其实可以的,有个很隐蔽的窍门隐藏在IQueryable<T>扩展方法对象Queryable中,也就是AsQueryable<T>方法,它返回的是一个实现了IQueryable<T>接口的EnumerableQuery对象,该对象的实现内容不是很复杂,将动态拼接的数据结构Expression<T>对象编译成可以执行的匿名函数,然后直接执行查询。[王清培版权所有,转载请给出署名]
我们来看一下EnumerableQuery对象的重点,它肯定有一个地方是将Expression<T>对象Compiler的地方。
- private IEnumerator<T> GetEnumerator()
- {
- if (this.enumerable == null)
- {
- EnumerableRewriter rewriter = new EnumerableRewriter();
- Expression<Func<IEnumerable<T>>> expression2 =
- Expression.Lambda<Func<IEnumerable<T>>>(rewriter.Visit(this.expression), (IEnumerable<ParameterExpression>) null);
- this.enumerable = expression2.Compile()(); //(1)重点
- }
- return this.enumerable.GetEnumerator();
- }
在上述代码中的“(1)重点”的地方,我们很清楚的看见表达式树被动态编译后然后紧接着又被执行,这里就能看出为什么IEnumerable<T>对象需要能够被转换成IQueryable<T>对象。这样就可以消除IEnumerable<T>、IQueryable<T>这两个接口之间的动态查询瓶颈。
为什么需要动态LINQ查询,上面说过问题出在我们没办法在运行时再去编写Lambda表达式了,都知道Lambda表达式到最后就是被编译成Expression表达式树对象,所以我们可以在运行时自己动态的构建Expression对象,这样就可以将动态构建出来的表达式树对象直接传入到需要的方法中。如果查询的数据对象是IEnumerable<T>则会被动态编译成可以执行的委托然后直接执行,如果查询的是IQueryable<T>则顺其自然的被提供程序解析执行。
下面我们来看一个简单的动态查询例子:
- 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="江苏淮安"}
- };
这是一组数据,为了简单测试就不搞那么麻烦的Linq to Sql数据源了。我们将要通过动态的构建表达式树来做为查询的逻辑,以往我们的Lambda在这个时候派不上用场了,在运行时我们无法再去构建委托类型。
现在的需求是从界面上接受一个Name值的输入,LINQ的查询只需要直接写就行了。
- var list = from i in StudentArrary where i.Name == "王清培" select i;
但是我们需要动态的构建表达式树来执行查询,表达式树的任何一个节点都有相对应的Expression派生类型,所以我们只要将相关类型组装起来就行了。由于我建的示例程序的类型是控制台程序,所以我们就用简短的方式演示一下如何构建表达式树。
- ParameterExpression parameter = Expression.Parameter(typeof(Student), "stu");//表示二元运算符的左边参数名称
- //表示"stu"参数的"stu.Name"中的Name属性,Name属性必须是反射获取的元数据才行,这样框架就才可以找到它
- MemberExpression property = Expression.MakeMemberAccess(parameter, typeof(Student).GetMember("Name")[0]);
- //表示常量值
- Console.WriteLine("请输入要查询人的名称:");
- ConstantExpression name = Expression.Constant(Console.ReadLine());//从用户输入流中读取值
- BinaryExpression binary = Expression.MakeBinary(ExpressionType.Equal, property, name);//拼接==运算符的左边、右边
- //完整的表达式是Lambda才对
- LambdaExpression lambda = Expression.Lambda(binary, parameter);
- //重要的就在这里,我们将完整的Lambda表达式直接变成可以执行的委托
- Func<Student, bool> wheredelegate = lambda.Compile() as Func<Student, bool>;
- //将编译后的可执行委托直接放入Where方法中执行
- var list2 = StudentArrary.AsQueryable<Student>().Where(wheredelegate);
- foreach (var i in list2)
- {
- Console.WriteLine("查询列表:");
- Console.WriteLine("姓名:{0},年龄:{1},地址:{2}", i.Name, i.Age, i.Address);
- }
- Console.ReadLine();
图例:
该例子的重点是如何动态构建逻辑,根据不同的项目要求完全可以将类似的功能封装起来供以后重复使用。如果觉得手动编写表达式树很麻烦的话,建议可以找一个辅助类能将Lambda表达式的对象树都能打印出来的工具,然后对着这棵树在去写就简单多了。
关于动态LINQ的第三方的API不是很多,比较常用的就是Dynamic.cs的使用,具体我没有用过,看过相关文档应该还是比较方便的。它的内部原理其实还是动态的构建表达式树,只不过这部分工作被人家做了,而我们使用起来却简单的很多。[王清培版权所有,转载请给出署名]
6】.DLR动态语言运行时(基于CLR之上的动态语言运行时)
从C#1一路走来,它变的越来越强大,.NET平台变得无所不能。很多人还一直咬着.NET不能跨平台,不能支持动态对象,不支持非托管等等理由来排斥它,然而他们所不知的是.NET已经悄无声息的做出来一大举动,那就是在静态语言运行时上嵌入动态语言运行时环境。我想不是微软不能支持所谓的缺点,而是它确实有它的本意。
动态语言运行时是在.NET4.0中引入的建立在CLR之上的运行时环境,目的是为了在静态语言中能够借鉴动态语言运行时的优点,比如强大的类型随意变换,这点在设计应用开发框架时尤其重要,任何一个好的特性都需要大面积的使用模式才能变的更完美。
说到动态运行时就不得不提JS中让人兴奋的var定义的对象特性了,如果没有留意在设计框架时而存在的烦恼其实很难发现动态运行和静态语言之间的好与不好。很明显的例子就是当我们定义一个数据类型的对象时,无法再在后期运行时对它进行其他类型的使用,看一个简单的例子:
- dynamic obj = 1;//整形
- obj = "1";//字符串
- obj = new { Name = "王清培", Age = 24, Address = "江苏" };//匿名对象类型
在运行时我们可以随意的设计对象的类型,我大胆的假设完全可以用动态运行时特性设计类似人工智能系统,提供基本原型,然后根据用户自己的思维方式构建任意对象树。技术科研是很不错的方向,企业应用可能还有待商讨。
以往我们很难在运行时为对象动态的添加属性、行为、事件,通过动态语言运行时我们可以很自如的添加想要的东西。
下面我们来看一个简单的例子,在运行时动态的构建一个对象类型,在以前我们只有用动态编译、CodeDom技术来实现,这里将变的很简单。
- static void Main(string[] args)
- {
- dynamic objModel = new ExpandoObject();//初始化可以动态添加属性、方法、事件的ExpandoObject对象
- objModel.Name = "王清培";//设置属性值
- objModel.Age = 24;
- objModel.WriteEvent = null;//存放事件的委托字段定义
- objModel.WriteEvent += new Action<string>(WriteName);//设置事件的方法
- objModel.WriteEvent(objModel.Name + objModel.Age);
- Console.ReadLine();
- }
- public static void WriteName(string info)
- {
- Console.WriteLine(info);
- }
一个很简单的例子告诉我们可以在C#中去编写如JS中的动态对象功能,不过目前还不是很成熟,动态对象的成员没有智能提示,应该是还没有被大面积使用起来,以后肯定也是一大美餐;[王清培版权所有,转载请给出署名]
总结:LINQ框架的基本使用原理就全部结束了,后面我们就来学习如何能让LINQ查询我们自定义的数据源。很多朋友都喜欢自己写ORM框架,那么你肯定少不了对LINQ的支持吧?后面我们就来详细的讲解如何扩展IQueryable<T>、IQueryableProvider<T>两个重量级接口,只有他们两个才能让我们和LINQ对话,这两个接口还是很神秘的。