.NET深入解析LINQ框架1

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了.NET深入解析LINQ框架1相关的知识,希望对你有一定的参考价值。

  • 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#的发展史,包括很多设计的历史渊源。来自大师的手笔,非常具有学习参考价值,不可多得的好书。

我们通过一个简短的小示例来快速的结束本小节。

  1. List<Order> OrderList = new List<Order>()   
  2.             {   
  3.                 new Order(){ Count=1},   
  4.                 new Order(){ Count=2},   
  5.                 new Order(){ Count=3}   
  6.             };   
  7.             foreach (Order order in OrderList)   
  8.             {   
  9.                 Console.WriteLine(order.Count);   
  10.             }  

这里我定义了一个List<Order>对象并且初始化了几个值,然后通过foreach迭代数据子项。其实这种写法很正常,也很容易理解。但是从C#3起加入了var关键字,编辑器对var关键字进行了自动分析类型的支持,请看下面代码。[王清培版权所有,转载请给出署名]

  1. var OrderList = new List<Order>()   
  2.             {   
  3.                 new Order(){ Count=1},   
  4.                 new Order(){ Count=2},   
  5.                 new Order(){ Count=3}   
  6.             };   
  7.             foreach (var order in OrderList)   
  8.             {   
  9.                 Console.WriteLine(order.Count);   
  10.             } 

编辑器可以智能的分析出我们定义是什么类型,换句话说在很多时候我们确实需要编辑器帮我们在编译时确定对象类型。这在LINQ中很常见,在你编写LINQ查询表达式时,你人为的去判断对象要返回的类型是很不现实的,但是由编译器来自动的根据语法规则进行分析就很理想化了。由于LINQ依赖于扩展方法,进行链式查询,所以类型在编写时是无法确定的。后面的文章将详细的讲解到,这里先了解一下。

  • 2.2.对象初始化器(简化了对象的创建及初始化的过程)

其实对象初始化器是一个简单的语法改进,目的还是为了方便我们进行对象的构造。(所谓万事俱备只欠东风,这个东风就是LINQ的方案。所以必须得先万事俱备才行。)

那么对象初始化器到底有没有多大的用处?我们还是先来目睹一下它的语法到底如何。

  1. var order = new Order() { Count = 10, OrderId = "123", OrderName = "采购单" };//属性初始化  
  2.  
  3. var OrderList = new List<Order>()   
  4.             {   
  5.                 new Order(){ Count=1, OrderId="1",OrderName="采购单"},   
  6.                 new Order(){ Count=2, OrderId="2",OrderName="采购单"},   
  7.                 new Order(){ Count=3, OrderId="3",OrderName="采购单"}   
  8.             };//集合初始化  
注意:对象初始化器只能用在属性、公共字段上。

属性初始化用这种语法编写的效果和直接用(order.Count=10;order.OrderId="123";order.OrderName="采购单";)是相等的。

集合初始化使用大括号的多行语法也很容易理解。类不具体的子对象的数据赋值是相同的。

我想对代码有追求的朋友都会很喜欢这种语法,确实很优美。[王清培版权所有,转载请给出署名]

  • 2.3.Lambda表达式(对匿名方法的改进,加入了委托签名的类型推断并很好的与表达式树的结合)

我想没有朋友对Lambda表达式陌生的,如果你对Lambda表达式陌生的也没关系,这里照看不误。后面再去补习一下就行了。

在LINQ的查询表达式中,到处都是Lambda造就的优雅。通过封装匿名方法来达到强类型的链式查询。

Lambda是函数式编程语言中的特性,将函数很简单的表示起来。不仅在使用时方便,查找定义也很方便。在需要的时候很简单定义就可以使用了,避免了在使用委托前先定义一个方法的繁琐。Lambda表达式与匿名委托在语法上是有区别的,当然这两者都是对匿名函数的封装。但是他们的出现是匿名委托早于Lambda。所以看上去还是Lambda显得优雅。

下面我们来看一个小示例,简单的了解一下Lambda的使用原理,最重要的是它优于匿名委托哪里?

  1. /// <summary>   
  2.  /// 按照指定的逻辑过滤数据   
  3.  /// </summary>   
  4.  public static IEnumerable<T> Filter<T>(IEnumerable<T> ObjectList, Func<T, bool> FilterFunc)   
  5.  {   
  6.      List<T> ResultList = new List<T>();   
  7.      foreach (var item in ObjectList)   
  8.      {   
  9.          if (FilterFunc(item))   
  10.              ResultList.Add(item);   
  11.      }   
  12.      return ResultList;    
  13.  } 

我们定义一个用来过滤数据的通用方法,这是个泛型方法,在使用时需要指定类型实参。方法有两个参数,第一个是要过滤的数据集合,第二个是要进行过滤的逻辑规则封装。

我们看一下调用的代码:

  1. int[] Number = new int[5] { 1, 2, 3, 4, 5 };   
  2. IEnumerable<int> result = Filter<int>(Number, (int item) => { return item > 3; });  
  3.  
  4. foreach (var item in result)   
  5.             {   
  6.                 Console.WriteLine(item);   
  7.             }  
我们这里定义的逻辑规则是,只要大于3的我就把提取出来并且返回。很明显这里的(int item) => { return item > 3; }语法段就是Lambda表达式,它很方便的封装了方法的逻辑。从这点上看Lambda明显要比匿名委托强大很多,最重要的是它还支持泛型的类型推断特性。[王清培版权所有,转载请给出署名]

那么什么是泛型的类型推断?

其实泛型的类型推断说简单点就是类型实参不需要我们显示的指定,编辑器可以通过分析表达式中的潜在关系自动的得出类型实参的类型。

说的有点空洞,我们还是看具体的代码比较清晰。

  1. int[] Number = new int[5] { 1, 2, 3, 4, 5 };   
  2. 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代码:

  1. /// <summary>   
  2.         /// 按照指定的逻辑过滤数据。具有延迟加载的特性。   
  3.         /// </summary>   
  4.         public static IEnumerable<T> FilterByYield<T>(IEnumerable<T> ObjectList, Func<T, bool> FilterFunc)   
  5.         {   
  6.             foreach (var item in ObjectList)   
  7.             {   
  8.                 if (FilterFunc(item))   
  9.                     yield return item;   
  10.             }   
  11.         } 

这里使用了yield关键字,使用它我们可以在方法内部形成一个自动的状态机结构。简单点讲也就是说系统会帮我们自动的实现一个继承了IEnumerable<T>接口的对象,在之前我们需要自己去实现迭代器接口成员,很费时费力而且性能不好。用这种方式定义的方法后,我们只有在遍历具体的集合时方法才会被调用,也算是一个很大的性能提升。[王清培版权所有,转载请给出署名]

泛型类型推断的不足之处;

当然类型推断还存在不足的地方,这里可以顺便参见一下我们老赵大哥的一篇文章:“C#编译器对泛型方法调用作类型推断的奇怪问题”;我在实际工作中也遇到过一个很头疼问题,这里顺便跟大家分享一下。按照常理说我在泛型方法的形参里面定义一个泛型的委托,他们的形参类型都是一样的占位符,但是如果我使用带有形参的方法作为委托的参数的话是无法进行类型推断的,然后使用无参数的方法作为委托参数是完全没有问题的。然后必须使用Lambda表达式才能做正确的类型推断,如果直接将带有参数的某个方法作为委托的参数进行传递是无法进行真确的类型推断,这里我表示很不理解。贴出代码与大家讨论一下这个问题。

我定义两个方法,这两个方法没有什么意义,只是一个有参数,一个没有参数。

无参数的方法:

  1. public static List<Order> GetOrderList()   
  2. {   
  3. return new List<Order>();   

有参数方法:

  1. public static List<Order> GetOrderListByModel(Order model)   
  2. {   
  3. return new List<Order>();   

Order对象只是一个类型,这里没有什么特别意义。

两个带有Func委托的方法,用来演示泛型的类型推断:

  1. public static TResult GetModelList<TResult>(Func<TResult> GetFunc)   
  2. {   
  3. return default(TResult);   
  4. }   
  5. public static TResult GetModelList<TSource, TResult>(Func<TSource, TResult> GetFunc)   
  6. {   
  7. 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实体进行扩展。[王清培版权所有,转载请给出署名]

我们来看一个小例子,看看扩展方法如果使用;

  1. public class OrderCollection   
  2. {   
  3.   public  List<Order> list = new List<Order>();   
  4. }   
  5. public class Order   
  6. {   
  7.     public int Count;   
  8.     public string OrderName;   
  9.     public string OrderId;   

这里仅仅是为了演示,比较简单。我定义了一个Order类和一个OrderCollection类,目前看来OrderCollection没有任何的方法,下面我们通过添加一个扩展方法来为OrderCollection类添加一写计算方法,比如汇总、求和之类的。

如何定义扩展方法?
扩展方法必须是静态类中的静态方法,我们定义一个OrderCollection类的扩展方法Count。

  1. public static class OrderExtend   
  2. {   
  3.     public static int Count(this OrderCollection OrderCollectionObject)   
  4.     {   
  5.         return OrderCollectionObject.list.Count;   
  6.     }   

扩展方法的第一个参数必须是this 关键开头然后经跟要扩展的对象类型,然后是扩展对象在运行时的实例对象引用。如果没有实例对象的引用我想扩展方法也毫无意识。所以这里我们使用Count方法来汇总一共有多少Order对象。通过OrderCollectionObject对象引用我们就可以拿到实例化的OrderCollection对象。

  1. OrderCollection orderCollection = new OrderCollection();   
  2. orderCollection.Count(); 

还有一个需要大家注意的是,如果我们定义的扩展方法在另外的命名空间里,我们在使用的时候一定要在当前的CS代码中应用扩展方法所在的命名空间,要不然编辑器是不会去寻找你目前在使用的对象的扩展方法的,切忌。这里还有一点是需要我们注意的,当我们在设计后期可能会被扩展方法使用的对象时需要谨慎的考虑对象成员访问权限,如果我们将以后可能会被扩展方法使用的对象设计成受保护的或者私有的,那么可能会涉及到无法最大力度的控制。[王清培版权所有,转载请给出署名]

  • 2.5.匿名类型(由对象初始化器推断得出的类型,该类型在编译后自动创建)

匿名类型其实也是比较好理解的,顾名思义匿名类型是没有类型定义的类型。这种类型是由编辑器自动生成的,仅限于当前上下文使用。废话少说了,我们还是看例子吧;

  1. var Student1 = new { Name = "王清培", Age = 24, Sex = "男", Address = "江苏淮安" };   
  2. var Student2 = new { Name = "陈玉和", Age = 23, Sex = "女", Address = "江苏盐城" }; 

定义匿名类型跟普通的定义类型差不多,只不过在new之后是一对大括号,然后经跟着你需要使用到的属性名称和值。

匿名类型的作用域;

匿名类型在使用上是有它先天性缺点的,由于缺乏显示的类型定义,所以无法在方法之间传递匿名类型。要想获取匿名类型的各属性值只能通过反射的方式动态的获取运行时的属性对象,然后通过属性对象去获取到属性的值。匿名类型在使用的时候才会被创建类型,所以它在运行时存在着完整的对象定义元数据,所以通过反射获取数据是完全可以理解的。

下面我们使用上面定义的类型来获取它的各个属性。

  1. PrintObjectProperty(Student1, Student2);  
  2.  
  3. public static void PrintObjectProperty(params object[] varobject)   
  4. {   
  5.     foreach (object obj in varobject)   
  6.     {   
  7.         foreach (System.Reflection.PropertyInfo property in obj.GetType().GetProperties())   
  8.         {   
  9.             Console.WriteLine(string.Format("PropertyName:{0},PropertyValue:{1}",   
  10.                 property.Name, property.GetValue(obj, null)));   
  11.         }   
  12.     }   
  13. }  

 

图例:

 

技术分享

通过反射的方式我们就可以顺利的获取到匿名类型的属性成员,然后通过属性信息在顺利的获取到属性的值。[王清培版权所有,转载请给出署名]

  • 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表达式目录树数据结构。我们看来例子;

  1. Func<int> Func = () => 10;   
  2. 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分别是用来筛选年龄和地址的。由于具体的方法代码比较简单这里就不贴出来了,目的是为了让大家能直观的看出链式设计模式的好处和灵活的地方。

 

示例代码:

 

  1. //构造Student数组   
  2. Student[] StudentArrary = new Student[3]   
  3. {   
  4.     new Student(){Name="王清培", Age=24, Sex="男", Address="江苏南京"},   
  5.     new Student(){Name="陈玉和", Age=23, Sex="女", Address="江苏盐城"},   
  6.     new Student(){Name="金源", Age=22, Sex="女", Address="江苏淮安"}   
  7. };   
  8. //使用Student数组初始化StudentCollection集合   
  9. StudentCollection StudentCollection = new StudentCollection(StudentArrary);   
  10. StudentCollection WhereCollection =   
  11.     StudentCollection.SelectByFemale().//筛选出所有女性学生列表   
  12.     SelectByAge(20).//筛选出年龄在20岁的学生列表   
  13.     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的扩展作为以前程序集的一个扩展程序集,在使用的时候可以使用或者可以不使用,只有这样我们才能使用扩展方法或者其他的新的语法特性。

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

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

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

示例代码:

 

  1. //构造Student数组   
  2. Student[] StudentArrary = new Student[3]   
  3. {   
  4.     new Student(){Name="王清培", Age=24, Sex="男", Address="江苏南京"},   
  5.     new Student(){Name="陈玉和", Age=23, Sex="女", Address="江苏盐城"},   
  6. 以上是关于.NET深入解析LINQ框架1的主要内容,如果未能解决你的问题,请参考以下文章

    .NET深入解析LINQ框架(五:IQueryableIQueryProvider接口详解)

    MMdetection框架速成系列 第03部分:简述整体构建细节与模块+训练测试模块流程剖析+深入解析代码模块与核心实现

    简述LINQ的发展历程

    .net 框架的基本层次结构及框架特点

    linq介绍及工作中应用两例——左联与内联,linq循环方法

    Android Bitmap深入介绍--- 开源加载框架简述