【转载】.NET深入解析LINQ框架(四:IQueryable、IQueryProvider接口详解)


.NET深入解析LINQ框架(四:IQueryable、IQueryProvider接口详解)

在开始看本篇文章之前先允许我打断一下各位的兴致。其实这篇文章本来是没有打算加“开篇介绍”这一小节的,后来想想还是有必要反馈一下读者的意见。经过前三篇文章的详细讲解,我们基本上对LINQ框架的构成原理有了一个根本的认识,包括对它的设计模型、对象的模型等,知道LINQ的查询表达式其实是C#之上的语法糖,不过这个糖确实不错,很方便很及时,又对一系列的LINQ支撑原理进行了大片理论的介绍,不知道效果如何;

在结束上一篇文章的时候,看到一个前辈评论说建议我多写写LINQ使用方面的,而不是讲这些理论。顺便借此机会解释一下,本人觉得LINQ的使用文章网上铺天盖地,实在没有什么必要更没有价值去写,网上的LINQ使用性的文章从入门到复杂的应用实在是太多了,不管是什么级别的程序员都能找到适用的文章。我更觉得这些文章属于使用类的,在实际项目中用到的时候稍微的查一下能用起来就行了,而重要的是能搞懂其原理才是我们长期所追求的,因为这些原理在任何一个应用框架的设计中都是相通的,可以帮助我们举一反三的学习,减少学习成本,不断的提高内在的设计思想。

所谓设计能力体现技术层次,这句话一点都不假。同志们我们不断追求的应该是设计,而不是拿来就用。当你搞懂了原理之后,我想每个人都能想出来各种不同的应用方向,那么技术发展才有意义,当然这也是最难能可贵的。

2】.扩展Linq to Object (应用框架具有查询功能)

下面我们通过具体的例子来分析一下上面的理论,先看看通过扩展方法来扩展系统的IEnumerable对象。

代码段:Order类

///  
/// 订单类型 
///  
public class Order 
{ 
    ///  
    /// 订单名称 
    ///  
    public string OrderName { get; set; } 
    ///  
    /// 下单时间 
    ///  
    public DateTime OrderTime { get; set; } 
    ///  
    /// 订单编号 
    ///  
    public Guid OrderCode { get; set; } 
}

这是个订单类纯粹是为了演示而用,里面有三个属性分别是"OrderName(订单名称)"、"OrderTime(下单时间)"、"OrderCode(订单编号)",后面我们将通过这三个属性来配合示例的完成。

如果我们是直接使用系统提供的IEnumerable对象的话,只需要构建IEnumerable对象的扩展方法就能实现对集合类型的扩展。我假设使用List来保存一批订单的信息,但是根据业务逻辑需要我们要通过提供一套独立的扩展方法来支持对订单集合数据的处理。这一套独立的扩展方法会跟随着当前系统部署,不作为公共的开发框架的一部分。这样很方便也很灵活,完全可以替代分层架构中的部分Service层、BLL层的逻辑代码段,看上去更为优雅。

再发散一下思维,我们甚至可以在扩展方法中做很多文章,把扩展方法纳入系统架构分析中去,采用扩展方法封装流线型的处理逻辑,对业务的碎片化处理、验证的链式处理都是很不错的。只有这样才能真正的让这种技术深入人心,才能在实际的系统开发当中去灵活的运用。

下面我们来构建一个简单的IEnumerable扩展方法,用来处理当前集合中的数据是否可以进行数据的插入操作。

代码段:OrderCollectionExtent静态类

public static class OrderCollectionExtent 
    { 
        public static bool WhereOrderListAdd(this IEnumerable IEnumerable) where T : Order 
        { 
            foreach (var item in IEnumerable) 
            { 
                if (item.OrderCode != null && !String.IsNullOrEmpty(item.OrderName) && item.OrderTime != null) 
                { 
                    continue; 
                } 
                return false; 
            } 
            return true; 
        } 
    }

OrderCollectionExtent是个简单的扩展方法类,该类只有一个WhereOrderListAdd方法,该方法是判断当前集合中的Order对象是否都满足了插入条件,条件判断不是重点,仅仅满足例子的需要。这个方法需要加上Order类型泛型约束才行,这样该扩展方法才不会被其他的类型所使用。

List orderlist = new List() 
            { 
                new Order(){ OrderCode=Guid.NewGuid(), OrderName="水果", OrderTime=DateTime.Now}, 
                new Order(){ OrderCode=Guid.NewGuid(), OrderName="办公用品",OrderTime=DateTime.Now} 
            }; 
            if (orderlist.WhereOrderListAdd()) 
            { 
                //执行插入 
            }

如果.NET支持扩展属性【不过微软后期肯定是会支持属性扩展的】,就不会使用方法来做类似的判断了。这样我们是不是很优雅的执行了以前BLL层处理的逻辑判断了,而且这部分的扩展方法是可以动态的更改的,完全可以建立在一个独立的程序集当中。顺便在扩展点使用思路,在目前MVVM模式中其实也可以将V中的很多界面逻辑封装在扩展方法中来减少VM中的耦合度和复杂度。包括现在的MVC都可以适当的采用扩展方法来达到更为便利的使用模式。

但是大部分情况下我们都是针对所有的IEnunerale类型进行扩展的,这样可以很好的结合Linq的链式编程。原理就这么多,根据具体项目需要适当的采纳。

2.2】.通过继承IEnumerable接口

这个小结主要将IEnumerable及它的扩展方法包括Linq的查询进行一个完整的结构分析,将给出详细的对象结构导图。

对象静态模型、运行时导图:

上图中的关键部分就是i==10将被封装成表达式直接送入Where方法,而select后面的i也是表达式【(int i)=>i】,也将被送入Select方法,这里就不画出来了。顺着数字序号理解,IEnumerable是Linq to Object的数据源,而Enumerable静态类是专门用来扩展Linq查询表达式中的查询方法的,所以当我们编写Linq查询IEnumerable集合是,其实是在间接的调用这些扩展方法,只不过我们不需要那么繁琐的去编写Lambda表达式,由编辑器帮我们动态生成。

小结:本节主要讲解了Linq to Object的原理,其实主要的原理就是Lambda表达式传入到Enumerable扩展方法当中,然后形成链式操作。Linq 只是辅助我们快速查询的语言,并不是.NET或者C#的一部分,在任何.NET平台上的语言中都可以使用。下面我们将重点分析Linq to Provider,这样我们才能真正的对LINQ进行高级应用。

3.】.实现IQueryable 、IQueryProvider接口

延迟加载的技术其实在Linq之前就已经在使用,只不过很少有人去关注它,都被隐藏在系统框架的底层。很多场合下我们需要自己去构建延迟加载特性的功能,在IEnumerable对象中构建延迟基本上是通过yield return 去构建一个状态机,当进行迭代的时候才进行数据的返回操作。那么在IQueryable中是通过执行Provider程序来获取数据,减少在一开始就获取数据的性能代价。IQueryable继承自IEnumerable接口,也就是可以被foreach语法调用的,但是在GetEnumerator方法中才会去执行提供程序的代码。我们来分析一下IQueryable接口的代码。

public IEnumerator GetEnumerator() 
       { 
           return (Provider.Execute(Expression) as IEnumerable).GetEnumerator(); 
       } 

       System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
       { 
           return (Provider.Execute(Expression) as IEnumerable).GetEnumerator(); 
       }

这是IQueryable接口中从IEnumerable继承下来的两个返回IEnumerator接口类型的方法,在我们目前使用的Linq to Sql、Linq to Entity中都会返回强类型的集合对象,一般都不会实时的进行数据查询操作,如果要想实时执行需要进行IQueryable.Provider.Execute方法的直接调用。

我们用图来分析一下Linq to Provider中的延迟加载的原理;

这段代码不会被立即执行,我们跟踪一下各个组成部分之间的执行过程;

这幅图重点是IQueryable对象的连续操作,大致原理是每次执行扩展方法的时候都会构造一个新的IQueryable,本次的IQueryable对象将包含上次执行的表达式树,以此类推就形成了一颗庞大的表达式树。详细的原理在下面几小节中具体分析。

最后Orderlist将是一个IQueryable类型的对象,该对象中包含了完整的表达式树,这个时候如果我们不进行任何的使用将不会触发数据的查询。这就是延迟加载的关键所在。如果想立即获取orderlist中的数据可以手动执行orderlist.Provider.Execute(orderlist.Expression)来获取数据。

3.2】.扩展方法的扩展对象之奥秘(this IQueryable source)

都知道Linq的查询是将一些关键字拼接起来的,行成连续的查询语义,这其中背后的原理文章上上下下也说过很多遍,我想也应该大致的了解了。其实这有点像是把大问题分解成多个小问题来解决,但是又不全是为了分解问题而这样设计,在链式查询中很多关键字在不同的查询上下文中都是公用的,比如where可以用在查询,也可以用在更新、删除。这里讨论的问题可能已经超过LINQ,但是很有意义,因为他们有着相似的设计模型。

根据3.2图中的意思,我们都已经知道扩展方法之间传输的对象都是来自不同的实例但是来自一个对象类型,那么为什么要分段执行每个关键字的操作呢?我们还是用图来帮助我们分析问题吧。

两行代码都引用了Where方法,都需要拼接条件,但是 Where方法所产生的条件不会影响你之前的方法。分段执行的好处就在这里,最大粒度的脱耦才能最大程度的重用。

3.4】.链式查询方法的设计误区(重点:一次执行程序多次处理)

在使用IQueryable时,我们尝试分析源码,看看IQueryable内部使用原理来帮我们生成表达式树数据的,我们顺其自然的看到了Provider属性,该属性是IQueryProvider接口,根据注释说明我们搞懂了它是最后执行查询的提供程序,我们理所当然的把IQueryable的开始实例当成了查询的入口,并且在连续调用的扩展方法当中它都保持唯一的一个实例,最后它完整的获取到了所有表达式,形成一颗表达式树。但是IQueryable却跟我们开了一个玩笑,它的调用到最后的返回不知道执行多少了CreateQuery了。看似一次执行却隐藏着多次方法调用,后台暗暗的构建了我们都不知道的执行模型,让人欣喜若狂。我们来揭开IQueryable在链式方法中到底是如何处理的,看看它到底藏的有多深。

public static IQueryable Where(this IQueryable source, Expressionbool>> predicate) 
{ 
    if (source == null) 
    { 
        throw Error.ArgumentNull("source"); 
    } 
    if (predicate == null) 
    { 
        throw Error.ArgumentNull("predicate"); 
    } 
    return source.Provider.CreateQuery(Expression.Call(null, ((MethodInfo) MethodBase.GetCurrentMethod()).MakeGenericMethod(new Type[] { typeof(TSource) }), new Expression[] { source.Expression, Expression.Quote(predicate) })); 
}

类似这段代码的在文章的上面曾出现过,大同小异,我们下面详细的分析一下它的内部原理,到底是如何构建一个动态却是静态的对象模型。

这个方法有一个参数,是条件表达式,并且这个方法扩展IQueryable接口,任何派生着都能直接使用。方法的返回类型也是IQueryable类型,返回类型和扩展类型相同就已经构成链式编程的最小环路。方法中有两个判断,第一个是判断是否是通过扩展方法方式调用代码的,防止我们直接使用扩展方法,第二个判断是确定我们是否提供了表达式。

那么重点是最后一行代码,它包裹着几层方法调用,到底是啥意思呢?我们详细分解后自然也就恍然大悟了。

由于问题比较复杂,这里不做全面的IQueryable的上下文分析,只保证本节的完整性。通过上图中,我们大概能分析出IQueryable对象是每次方法的调用都会产生一个新的实例,这个实例接着被下一个方法自然的接受,依次调用。

面向接口的设计追求职责分离,这里为什么把执行和创建IQueryable都放到IQueryProvider中去?如果把创建IQueryable提取处理形成独立的创建接口我觉得更巧妙,当然这只是我的猜测,也许是理解错了。 

作者:王清培

出处:http://www.cnblogs.com/wangiqngpei557/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利