zoukankan      html  css  js  c++  java
  • 再讲IQueryable<T>,揭开表达式树的神秘面纱

    接上篇《先说IEnumerable,我们每天用的foreach你真的懂它吗?

    最近园子里定制自己的orm那是一个风生水起,感觉不整个自己的orm都不好意思继续混博客园了(开个玩笑)。那么在此之前我们有必要仔细了解下 IQueryable<T> ,于是就有了此文。

    什么是树?

    什么是树?这个问题好像有点白痴。树不就是树嘛。看图:

    我们从最下面的主干开始往上看,主枝-分支-分支....可以说是无限分支下去。我们倒过来看就是这样:

    平时我们用得最多的树结构数据就是XML了,节点下面可以无限添加子节点。我们想想平时还用过什么树结构数据,比如:菜单无限分级、评论区的楼层。

    这和我们今天讲的有毛关系啊。... 我们今天主要就是来分析表达式树的。、

    lambda表达式和表达式树的区别:

    Lambda表达式:

    Func<Student, bool> func = t => t.Name == "农码一生";

    表达式树: 

    Expression<Func<Student, bool>> expression = t => t.Name == "农码一生"; 

     咋一看,没啥区别啊。表达式只是用Expression包了一下而已。那你错了,这只是Microsoft给我们展示的障眼法,我们看编译后的C#代码:

    第一个lambda表达式编译成了匿名函数,第二个表达式树编译成一了一堆我们不认识的东西,远比我们原来写的lambda复杂得多。

    结论:

    • 我们平时使用的表达式树,是编写的lambda表达式然后编译成的表达式树,也就是说平时一般情况使用的表达式树都是编译器帮我们完成的。(当然,我们可以可以手动的主动的去创表达式树。只是太麻烦,不是必要情况没有谁愿意去干这个苦活呢

    我们来看看表达式树到底有什么神奇的地方:

    有没有看出点感觉来?Body里面有Right、Left,Right里面又有Right、Left,它们的类型都是继承自 Expression 。这种节点下面有节点,可以无限附加下去的数据结构我们称为树结构数据。也就是我们的表达式树。

    补:上面的 Student 实体类:

    public class Student
    {
        public string Name { get; set; }
    
        public int Age { get; set; }
    
        public string Address { get; set; }
    
        public string Sex { get; set; }
    }
    View Code

    解析表达式树

    上面我们看到了所谓的表达式树,其他也没有想象的那么复杂嘛。不就是一个树结构数据嘛。如果我们要实现自己的orm,免不了要解析表达式树。一般说到解析树结构数据都会用到递归算法。下面我们开始解析表达式树。

    先定义解析方法:

    //表达式解析
    public static class AnalysisExpression
    {
        public static void VisitExpression(Expression expression)
        {
            switch (expression.NodeType)
            {
                case ExpressionType.Call://执行方法
                    MethodCallExpression method = expression as MethodCallExpression;
                    Console.WriteLine("方法名:" + method.Method.Name);
                    for (int i = 0; i < method.Arguments.Count; i++)
                        VisitExpression(method.Arguments[i]);
                    break;
                case ExpressionType.Lambda://lambda表达式
                    LambdaExpression lambda = expression as LambdaExpression;
                    VisitExpression(lambda.Body);
                    break;
                case ExpressionType.Equal://相等比较
                case ExpressionType.AndAlso://and条件运算
                    BinaryExpression binary = expression as BinaryExpression;
                    Console.WriteLine("运算符:" + expression.NodeType.ToString());
                    VisitExpression(binary.Left);
                    VisitExpression(binary.Right);
                    break;
                case ExpressionType.Constant://常量值
                    ConstantExpression constant = expression as ConstantExpression;
                    Console.WriteLine("常量值:" + constant.Value.ToString());
                    break;
                case ExpressionType.MemberAccess:
                    MemberExpression Member = expression as MemberExpression;
                    Console.WriteLine("字段名称:{0},类型:{1}", Member.Member.Name, Member.Type.ToString());
                    break;
                default:
                    Console.Write("UnKnow");
                    break;
            }
        }
    
    }

    调用解析方法:

    Expression<Func<Student, bool>> expression = t => t.Name == "农码一生" && t.Sex == "";
    AnalysisExpression.VisitExpression(expression);

    我们来看看执行过程:

    一层一层的往子节点递归,直到遍历完所有的节点。最后打印效果如下:

    基本上我们想要的元素和值都取到了,接着怎么组装就看你自己的心情了。是拼成sql,还是生成url,请随意!

    实现自己的IQueryable<T>、IQueryProvider

    仅仅解析了表达式树就可以捣鼓自己的orm了?不行,起码也要基于 IQueryable<T> 接口来编码吧。

    接着我们自定义个类 MyQueryable<T> 继承接口 IQueryable<T> :

     public class MyQueryable<T> : IQueryable<T>
     {
         public IEnumerator<T> GetEnumerator()
         {
             throw new NotImplementedException();
         }
         IEnumerator IEnumerable.GetEnumerator()
         {
             throw new NotImplementedException();
         }
         public Type ElementType
         {
             get { throw new NotImplementedException(); }
         }
         public Expression Expression
         {
             get { throw new NotImplementedException(); }
         }
         public IQueryProvider Provider
         {
             get { throw new NotImplementedException(); }
         }
     }

    我们看到其中有个接口属性 IQueryProvider ,这个接口的作用大着呢,主要作用是在执行查询操作符的时候重新创建 IQueryable<T> 并且最后遍历的时候执行sql远程取值。我们还看见了 Expression  属性。

    现在我们明白了 IQueryable<T> 和 Expression (表达式树)的关系了吧:

    •  IQueryable<T> 最主要的作用就是用来存储 Expression(表达式树)

    下面我们也自定义现实了 IQueryProvider 接口的类 MyQueryProvider :

    public class MyQueryProvider : IQueryProvider
    {
        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            throw new NotImplementedException();
        }
        public IQueryable CreateQuery(Expression expression)
        {
            throw new NotImplementedException();
        }
        public TResult Execute<TResult>(Expression expression)
        {
            throw new NotImplementedException();
        }
        public object Execute(Expression expression)
        {
            throw new NotImplementedException();
        }
    }

    上面全是自动生成的伪代码,下面我们来填充具体的实现:

        public class MyQueryProvider : IQueryProvider
        {
            public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
            {
                return new MyQueryable<TElement>(expression);
            }
    
            public IQueryable CreateQuery(Expression expression)
            {
                throw new NotImplementedException();
            }
    
            public TResult Execute<TResult>(Expression expression)
            {
                return default(TResult);
            }
    
            public object Execute(Expression expression)
            {
                return new List<object>();
            } 
        }  
        public class MyQueryable<T> : IQueryable<T>
        {
            public MyQueryable()
            {
                _provider = new MyQueryProvider();
                _expression = Expression.Constant(this);
            }
    
            public MyQueryable(Expression expression)
            {
                _provider = new MyQueryProvider();
                _expression = expression;
            }
            public Type ElementType
            {
                get { return typeof(T); }
            }
    
            private Expression _expression;
            public Expression Expression
            {
                get { return _expression; }
            }
    
            private IQueryProvider _provider;
            public IQueryProvider Provider
            {
                get { return _provider; }
            }
    
            public IEnumerator GetEnumerator()
            {
                return (Provider.Execute(Expression) as IEnumerable).GetEnumerator();
            }
    
            IEnumerator<T> IEnumerable<T>.GetEnumerator()
            {
                var result = _provider.Execute<List<T>>(_expression);
                if (result == null)
                    yield break;
                foreach (var item in result)
                {
                    yield return item;
                }
            }
        }
    View Code

    执行代码:

     var aa = new MyQueryable<Student>();
     var bb = aa.Where(t => t.Name == "农码一生");
     var cc = bb.Where(t => t.Sex == "");
     var dd = cc.AsEnumerable();
     var ee = cc.ToList(); 

    接着我们看看执行过程:

    结论:

    • 每次在执行 Where 查询操作符的时候 IQueryProvider 会为我们创建一个新的 IQueryable<T> 
    • 调用 AsEnumerable() 方法的时候并不会去实际取值(只是得到一个IEnumerable)[注意:在EF里面查询不要先取IEnumerable后滤筛,因为AsEnumerable()会生成查询全表的sql]
    • 执行 ToList() 方法时才去真正调用迭代器 GetEnumerator() 取值
    • 真正取值的时候,会去执行 IQueryProvider 中的 Execute 方法。(就是在调用这个方法的时候解析表达式数,然后执行取得结果)

    我们看到真正应该办实事的 Execute  我们却让他返回默认值了。

    现在估计有人不爽了,你到是具体实现下 Execute 。好吧!(其实通过上面说的解析表达式树,你可以自己在这里做想做的任何事了。)

    首先为了简单起见,我们用一个集合做为数据源:

    //构造Student数组
    public static List<Student> StudentArrary = new List<Student>()
    {
            new Student(){Name="农码一生", Age=26, Sex="", Address="长沙"},
            new Student(){Name="小明", Age=23, Sex="", Address="岳阳"},
            new Student(){Name="嗨-妹子", Age=25, Sex="", Address="四川"}
    };

    然后,重新写一个VisitExpression2方法:(和之前的区别: 现在目的是取表达式树中的表达式,而不是重新组装成sql或别的)

    public static void VisitExpression2(Expression expression, ref List<LambdaExpression> lambdaOut)
    {
        if (lambdaOut == null)
            lambdaOut = new List<LambdaExpression>();
        switch (expression.NodeType)
        {
            case ExpressionType.Call://执行方法
                MethodCallExpression method = expression as MethodCallExpression;
                Console.WriteLine("方法名:" + method.Method.Name);
                for (int i = 0; i < method.Arguments.Count; i++)
                    VisitExpression2(method.Arguments[i], ref  lambdaOut);
                break;
            case ExpressionType.Lambda://lambda表达式
                LambdaExpression lambda = expression as LambdaExpression;
                lambdaOut.Add(lambda);
                VisitExpression2(lambda.Body, ref  lambdaOut);
                break;
            case ExpressionType.Equal://相等比较
            case ExpressionType.AndAlso://and条件运算
                BinaryExpression binary = expression as BinaryExpression;
                Console.WriteLine("运算符:" + expression.NodeType.ToString());
                VisitExpression2(binary.Left, ref  lambdaOut);
                VisitExpression2(binary.Right, ref  lambdaOut);
                break;
            case ExpressionType.Constant://常量值
                ConstantExpression constant = expression as ConstantExpression;
                Console.WriteLine("常量值:" + constant.Value.ToString());
                break;
            case ExpressionType.MemberAccess:
                MemberExpression Member = expression as MemberExpression;
                Console.WriteLine("字段名称:{0},类型:{1}", Member.Member.Name, Member.Type.ToString());
                break;
            case ExpressionType.Quote:
                UnaryExpression Unary = expression as UnaryExpression;
                VisitExpression2(Unary.Operand, ref  lambdaOut);
                break;
            default:
                Console.Write("UnKnow");
                break;
        }
    }

    然后重新实现方法 Execute 

    public TResult Execute<TResult>(Expression expression)
    {
        List<LambdaExpression> lambda = null;
        AnalysisExpression.VisitExpression2(expression, ref lambda);//解析取得表达式数中的表达式
        IEnumerable<Student> enumerable = null;
        for (int i = 0; i < lambda.Count; i++)
        {
            //把LambdaExpression转成Expression<Func<Student, bool>>类型
            //通过方法Compile()转成委托方法
            Func<Student, bool> func = (lambda[i] as Expression<Func<Student, bool>>).Compile(); 
            if (enumerable == null)
                enumerable = Program.StudentArrary.Where(func);//取得IEnumerable
            else
                enumerable = enumerable.Where(func);
        }
        dynamic obj = enumerable.ToList();//(注意:这个方法的整个处理过程,你可以换成解析sql执行数据库查询,或者生成url然后请求获取数据。)
        return (TResult)obj;
    }

    执行过程:

    个人对 IQueryable 延迟加载的理解:

    • 前段部分的查询操作符只是把逻辑分解存入表达式树,并没有远程执行sql。
    • foreache执行的是 IEnumerable<T> ,然而 IEnumerable<T> 同样具有延迟加载的特性。每次迭代的时候才真正的取数据。且在使用导航属性的时候会再次查询数据库。(下次说延迟加载不要忘记了 IEnumerable 的功劳哦!)

    小知识:

    表达式树转成Lambda表达式:

    Expression<Func<Student, bool>> expression = t => t.Name == "农码一生";
    Func<Student, bool> func = expression.Compile();

    总结:

    表达式树的分析就告一段落了,其中还有很多细节或重要的没有分析到。下次有新的心得再来总结。

    感觉表达式树就是先把表达式打散存在树结构里(一般打散的过程是编译器完成),然后可以根据不同的数据源或接口重新组装成自己想要的任何形式,这也让我们实现自己的orm成为了可能。

    今天主要是对表达式树的解析、和实现自己的IQueryable<T>、IQueryProvider做了一个记录和总结,其中不定有错误的结论或说法,轻点拍!

    demo下载:http://pan.baidu.com/s/1nvAksgL 

    本文以同步至索引目录:《C#基础知识巩固

    推荐阅读:

    http://www.cnblogs.com/jesse2013/p/expressiontree-part1.html

    http://www.cnblogs.com/jesse2013/p/expressiontree-part2.html

    http://www.cnblogs.com/jesse2013/p/expressiontree-Linq-to-cnblogs.html

    园友@风口上的猪推荐:

    http://www.cnblogs.com/Ninputer/archive/2009/09/08/expression_tree3.html
    http://blog.zhaojie.me/2009/03/expression-cache-1.html

  • 相关阅读:
    leetcode 13. Roman to Integer
    python 判断是否为有效域名
    leetcode 169. Majority Element
    leetcode 733. Flood Fill
    最大信息系数——检测变量之间非线性相关性
    leetcode 453. Minimum Moves to Equal Array Elements
    leetcode 492. Construct the Rectangle
    leetcode 598. Range Addition II
    leetcode 349. Intersection of Two Arrays
    leetcode 171. Excel Sheet Column Number
  • 原文地址:https://www.cnblogs.com/zhaopei/p/IQueryable-IQueryProvider.html
Copyright © 2011-2022 走看看