Expression_Tree上手指南.doc_第1页
Expression_Tree上手指南.doc_第2页
Expression_Tree上手指南.doc_第3页
Expression_Tree上手指南.doc_第4页
Expression_Tree上手指南.doc_第5页
已阅读5页,还剩7页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

Expression Tree上手指南 (一) 大家可能都知道Expression Tree是.NET 3.5引入的新增功能。不少朋友们已经听说过这一特性,但还没来得及了解。看看博客园里的老赵等诸多牛人,将Expression Tree玩得眼花缭乱,是否常常觉得有点落伍了呢?其实Expression Tree是一个一点就透的特性,只要对其基本概念有了一定的了解,就可以自己发挥出无数的用法。特别是之前对Reflection,泛型等知识有过一些了解的话,就会发现Expression Tree的加入绝对是你工作中的得力助手。如果你是Expression Tree的新手,那么从本文开始,你就可以领略这一工具,之后再看老赵的文章就从容不迫了从表达式说起Expression Tree从名称上看就是“表达式树”的意思。许多人一看到它,就会想起Lambda表达式,委托,Linq等等一堆名词。但其实最基本的概念就是“表达式”。现在让我们把那些名词全都给忘了,来重新了解一下表达式。表达式是当今编程语言中最重要的组成成分。简单的说,表达式就是变量、数值、运算符、函数组合起来,表示一定意义的式子。例如下面这些都是(C#)的表达式:3 /常数表达式a /变量或参数表达式!a /一元逻辑非表达式a + b /二元加法表达式Math.Sin(a) /方法调用表达式new StringBuilder() /new 表达式此外还有取地址表达式,新建数组表达式,赋值表达式等许多种。如你所见,表达式常常能够表示一个值或对象,因此在C#这类强类型语言里,表达式常常有一个相应的类型。例如“3”这个表达式就是int类型的。不过有时表达式也没有值,例如方法调用表达式,如果方法没有返回值的话这个表达式也就没有值。这种情况我们也说表达式的类型是void。表达式的一个重要的特点就是它可以无限地组合,只要符合正确的类型和语义。例如+可以用于各类数值类型的加法,那么加号的左右就可以是任何类型为相应数值的表达式。可以是函数调用和常数:Math.Sin(a) + 3;也可以是同样的加法表达式a + 2 + 3。想必大家在实践中早就用上这个特性了。那么a + 2 + 3是如何计算出正确的值来的呢?应该首先计算(a + 2)的结果b,然后计算b + 3的值。如果我们用一个图来表示这个过程,它就像这样: 同理,表达式Math.Sin(a) + 3也可以表示成这样:如你所见,所有表达式都可以表示成这种树一样的结构。每个节点和它所有的后裔都构成一个独立的表达式。如果我们将表达式表示成这种结构,就可以轻易地明白它的运算规则和步骤。因此我们可以用一种树状的数据结构来表示每一个表达式。这个数据结构就是表达式树。表达式树刚才提到了,表达式树就是一种表示表达式的数据结构。System.Linq.Expression命名空间下的Expression类和它的诸多子类就是这一数据结构的实现。每个表达式都可以表示成Expression某个子类的实例。每个Expression子类都按照相应表达式的特点储存自己的子节点。例如BinaryExpression就表示各种二元运算符的表达式。它的Left和Right属性就是参与二元运算的两个运算数。下面开始我们将每种表达式的内部特定结构称作表达式的“成分”。比如二元运算表达式的成分就是左运算数表达式、右运算符表达式和一个运算符。Expression各个子类的构造函数都是不公开的,要创建表达式树只能使用Expression类提供的静态方法。(这同时也说明表达式树体系是不能自己扩展的)如果我们要创建1 + 2 + 3这个表达式的表达式树,可以这样写:ConstantExpression exp1 = Expression.Constant(1);ConstantExpression exp2 = Expression.Constant(2);BinaryExpression exp12 = Expression.Add(exp1, exp2);ConstantExpression exp3 = Expression.Constant(3);BinaryExpression exp123 = Expression.Add(exp12, exp3);这个应该非常好理解。下面如果我们想写出Math.Sin(a)这个表达式的表达式树怎么办呢?这时问题就来了,这里面的“a”不知道该用什么表示。为了解决这个问题,下面Lambda表达式该登场了。Lambda表达式Lambda也是C#3.0/VB9新引入的表达式。我们都知道它和以前的匿名函数和委托有关。不过现在还是把这些暂时都忘掉,完全把Lambda表达式当成一种新的表达式来看到。刚才我们看到了各种各样的表达式,有的表示一个常数;有的表示一个变量;有的表示加法;有的表示函数调用等等。Lambda表达式作为一个表达式,它表达的是一个函数。Lambda表达式的成分就是一系列的参数加上一个表示函数逻辑的表达式组成。(parameters) = expressionLambda表达式最重要的特色是它可以引入一批参数,这批参数可以在函数体表达式中使用。基于这种特色,我们就可以创建出带自定义变量的表达式树,而这些自定义变量就表示成Lambda表达式的参数:ParameterExpression expA = Expression.Parameter(typeof(double), a); /参数aMethodCallExpression expCall = Expression.Call(null, typeof(Math).GetMethod(Sin, BindingFlags.Static | BindingFlags.Public), expA); /Math.Sin(a)LambdaExpression exp = Expression.Lambda(expCall, expA); / a = Math.Sin(a)我们这里使用了一个新的Expression树节点MethodCallExpression。它可以表示一次方法调用。方法是使用MethodInfo实例来表示的。如果画成图的话,Lambda表达式可以画成这样: 由此可见,用Lambda表达式表示函数是一个非常直观的过程。有时候我们真的觉得没有名字的函数才是真正的函数。因为函数只需要参数和函数体两个成分即可,名称只是为了在别处引用它才需要的。到此为止,我们已经理解了表达式树的基本概念。但是我们还只能用最原始的方法一步一步地构建表达式树。前面我们用到的LambdaExpression是适用于各种类型函数的类,.NET还提供了一种适用于特定委托类型的LambdaExpression类型。我们用它来表示强类型的LambdaExpression。现在我们就要引入C#、VB真正表达式和表达式树之间的桥梁表达式树字面量(Expression Tree Literals),可以自动从Lambda表达式生成它的表达式树:ExpressionFunc exp = a = Math.Sin(a);注意这个赋值语句,左侧是一个强类型的LambdaExpression:ExpressionFunc,右侧是一个真正的C#语法的Lambda表达式。C#的编译器在这种情况下就能自动为你生成右侧Lambda表达式的表达式树。也就是说,这个exp和我们刚才手工生成Lambda表达式树基本是一样的。注意,这种特殊的语法仅能从Lambda表达式获得表达式树。别的表达式是不能自动生成表达式树的。但是一旦我们获得了Lambda表达式,就可以直接从它的子节点获得内部表达式了。这是一个非常有用的语法,要深刻理解它的作用。需要注意的是,这里的委托类型Func有双重作用,首先它限定生成的表达式树是一个接受double,并返回double的一元Lambda函数;其次这个委托可以直接用在Lambda表达式树的编译当中,可在C#作强类型处理。我们后面谈到表达式树的编译时再详细的讨论这个问题。表达式树的意义:数据化的表达式我们现在已经能够用两种方式创建表达式树用Expression的节点组合或者直接从C#、VB的Lambda表达式生成。不管使用的是那种方法,最后我们得到的是一个内存中树状结构的数据。如果我们愿意,可以将它还原成文本源代码的表达式或者序列化到字符串里。注意,如果是C#的表达式本身,我们是没法对它进行输出或者序列化的,它只存在于编译之前的源文件里。现在的表达式树已经成为了一种数据,而不在是语言中的表达式。我们可以在运行的时候处理这一数据,精确了解其内在逻辑;将它传递给其他的程序或者再次编译成为可以执行的代码。这就可以总结为表达式树的三大基本用途:o 运行时分析表达式的逻辑 o 序列化或者传输表达式 o 重新编译成可执行的代码在下一篇中,我们将着重介绍这三者在实际开发中的用途。习题还有习题?别担心,你可以将下列问题当做上机实践的素材,以便很快地理解本次学到的内容。第一题:画出下列表达式的表达式树。一开始,您很可能不知道某些操作其实也是表达式(比如取数组的运算符a2),不过没有关系,后面的习题将帮你验证这一点。-aa + b * 2Math.Sin(x) + Math.Cos(y)new StringBuilder(“Hello”)new int a, b, a + bai 1 * ia.Length b | b = 0(高难度)new System.Windows.Point() X = Math.Sin(a), Y = Math.Cos(a) 提示:注意运算符的优先级。倒数第二题的a是String类型,其余变量你可以用任意合适的简单类型。如果想知道以上表达式分别是什么表达式,可以查MSDN。第二题:将上述表达式中的变量提取成参数,表示成Lambda表达式的形式。然后用Expression静态方法逐渐组合的方式将他们构建出来。例如a + b * 2写成Lambda表达式就成了(int a, int b)= a + b * 2。按照前面Math.Sin(a)例子的做法用Expression的方法组合出这一逻辑。第三题:验证您第二题的结果。请将生成Expression实例ToString(),它就会显示出它的表达式原型。看看您构建的表达式ToString()出来是不是正确。如果您发现生成的Expression不是你想要构建的,又不知道该怎么做的话,可以用表达式树字面量的语法让C#编译器帮您生成。然后用Reflector反编译它就能看到正确的表达式树。不过C#编译器有时会使用一些作弊手法,聪明的你应该能找到绕过的手段Expression Tree 上手指南 (二) 上回我们说到Expression Tree是一种表示编程语言中“表达式”概念的树状数据结构,并且学习了从Lambda表达式自动生成表达式树的C#语法。那么它到底有什么用呢?其实上一回已经提到了Expression Tree的基本功能:分析表达式的逻辑、保存和传输表达式以及重新编译表达式。现在我们就分别来看这三项基本功能如何使用。分析表达式的逻辑表达式树中已经包含了表达式所需的各种成分,我们只需要像遍历树一样遍历表达式树,就可以解析表达式的含义。我们现在就来动手解析一下四则运算的表达式树。假设表达式中仅有加减乘除运算。比方说我们有这个一个表达式(a, b, m, n) = m * a * a + n * b * b,它的表达式树是这样:最外层是LambdaExpression,我们很容易就可以从它的Body属性得到里面的表达式。在限定条件下,这个表达式只含有BinaryExpression一种表达式。我们只需要要遍历这棵树,在遇到BinaryExpression的时候完成所需的运算即可。我们此处采用的是后序遍历的逻辑:class Program static void Main(string args) ExpressionFunc myExp = (a, b, m, n) = m * a * a + n * b * b; var calc = new BinaryExpressionCalculator(myExp); Console.WriteLine(calc.Calculate(1, 2, 3, 4); class BinaryExpressionCalculator Dictionary m_argDict; LambdaExpression m_exp; public BinaryExpressionCalculator(LambdaExpression exp) m_exp = exp; public double Calculate(params double args) /从ExpressionExpression中提取参数,和传输的实参对应起来。 /生成的字典可以方便我们在后面查询参数的值 m_argDict = new Dictionary(); for (int i = 0; i m_exp.Parameters.Count; i+) /就不检查数目和类型了,大家理解哈 m_argDictm_exp.Parametersi = argsi; /提取树根 Expression rootExp = m_exp.Body as Expression; /计算! return InternalCalc(rootExp); double InternalCalc(Expression exp) ConstantExpression cexp = exp as ConstantExpression; if (cexp != null) return (double)cexp.Value; ParameterExpression pexp = exp as ParameterExpression; if (pexp != null) return m_argDictpexp; BinaryExpression bexp = exp as BinaryExpression; if (bexp = null) throw new ArgumentException(不支持表达式的类型, exp); switch (bexp.NodeType) case ExpressionType.Add: return InternalCalc(bexp.Left) + InternalCalc(bexp.Right); case ExpressionType.Divide: return InternalCalc(bexp.Left) / InternalCalc(bexp.Right); case ExpressionType.Multiply: return InternalCalc(bexp.Left) * InternalCalc(bexp.Right); case ExpressionType.Subtract: return InternalCalc(bexp.Left) - InternalCalc(bexp.Right); default: throw new ArgumentException(不支持表达式的类型, exp); 我们用了一个递归逻辑实现了遍历,为了方便封装了一个简单的类。这个简单的程序就可以计算任意double型参数的四则运算表达式。大家可能要问了,C#本身就具有运算四则运算表达式的能力,为什么我们要亲自解析表达式树来做到的同样的功能呢?如果仅仅是计算表达式的值,当然不用自己来解析。但自己解析的一大好处就是可以在解析过程中加入自定义的语义动作。比如我们只要稍微更改一下上述程序中的InternalCalc就能输出Lambda表达式的前缀序列:string InternalPrefix(Expression exp) ConstantExpression cexp = exp as ConstantExpression; if (cexp != null) return cexp.Value.ToString(); ParameterExpression pexp = exp as ParameterExpression; if (pexp != null) return pexp.Name; BinaryExpression bexp = exp as BinaryExpression; if (bexp = null) throw new ArgumentException(不支持表达式的类型, exp); switch (bexp.NodeType) case ExpressionType.Add: return + + InternalPrefix(bexp.Left) + + InternalPrefix(bexp.Right); case ExpressionType.Divide: return - + InternalPrefix(bexp.Left) + + InternalPrefix(bexp.Right); case ExpressionType.Multiply: return * + InternalPrefix(bexp.Left) + + InternalPrefix(bexp.Right); case ExpressionType.Subtract: return / + InternalPrefix(bexp.Left) + + InternalPrefix(bexp.Right); default: throw new ArgumentException(不支持表达式的类型, exp); 您可以尝试一下将自己的四则运算表达式转换成前缀序列是什么样子,有点函数式语言的味道了 我们还可以改变上述程序,让他实时输出四则运算的中间结果等。大家可以试验一下。由此可见,我们分析表达式树并不一定是要计算表达式的值,我们可以在翻译表达式的时候执行任何自定义的语义动作,达到翻译表达式或用表达式驱动特殊代码的作用。Linq to Sql就是这个用途的典型例子。我们知道Linq to Sql的QueryProvider可以将IQueryable上的一系列查询动作翻译成SQL语句。那么表达式树是如何充当这个角色的呢?下面我们来看一个Queryable的例子:IQueryable source = .var query = from d in source where d 0 select d * 2;这个查询语句等价于如下直接使用Queryable扩展方法的代码:var query = source.Where(d = d 0).Select(d = d * 2);和Enumerable类型的扩展方法不同,Queryable扩展方法的每个操作都接受一个强类型的LambdaExpression参数。因此上述代码中的Where和Select分别接受了一个Expression Tree作为参数。最后的query对象中包含了整个序列每个操作对应的表达式树,QueryProvider就可以通过分析表达式树,翻译成SQL或者直接进行查询动作了。这种用法有点类似于解释器模式。你可以用Expression Tree来表示一种逻辑,然后解析它来驱动你的业务逻辑。原本属于具体语言的表达式现在成为了我们可以直接利用的一大利器。序列化和传输表达式通过上文我们学习了解析表达式树并且执行自定义动作的方法。在实际应用中,我们还可能遇到以下需求:1. 生成表达式的程序与解析表达式的程序不在同一进程内(例如在客户端生成表达式,在服务端解析)。2. 需要储存或缓存表达式,为以后多次使用做准备。3. 需要用其他非.NET技术处理或生成表达式树。以上需求需要将表达式树当成纯数据来看待。内存中保存表达式树固然没有问题,我们还需要探究表达式树序列化的问题。.NET 3.5版本的表达式树本身并没有特别优雅的序列化方式,它并不支持DataContract序列化。因为表达式的种类繁多,许多表达式都和CLR具体类型相关。通常的做法是,根据自己要使用的表达式子集手动编写序列化的代码。ToString()也可以得到一个表示表达式的字符串,可惜他不是那么容易变回表达式树。所以ToString()通常仅仅作为显示和参考。MSDN Code Gallery中有一个LuckH提供的通用Expression Tree序列化例子。它可以提供比较清晰地XML序列化结果。关于Expression Tree缓存,可以看老赵的这一系列文章,你可以学到各种利用表达式树自身特性的有趣用法。习题1. 基于本文提供的四则运算例子,给他加上支持一元正负运算符和函数调用的功能。这样你就能计算Math.Sin(a) + b这样的表达式了。可以先仅支持静态函数。2. 改写这个程序,使之能根据四则运算表达式树生成javascript代码。3. 你可以试着将以上代码扩展成一个小小的类库,能用javascript来执行你提供的四则运算表达式。Expression Tree 上手指南 (三) 上回我们说到手工解析Expression Tree,以便获得其中的逻辑或者执行我们自定义的语义动作。这种做法扩展了C#语言的威力,让我们可以用C#的语法来做更多的事情,例如Linq to Sql。今天我们要学习一种相反的做法,手工创建表达式树,然后让.NET来解析它。这是一种强大的动态编程手段。我们可以用它来完成许多以前需要Reflection.Emit才能完成的任务。LambdaExpression的独有方法:Compile这里我们仍要使用诸多表达式中与众不同的LambdaExpression。在Visual Studio中我们可以看到他有一个Compile方法。这是做什么的呢。在第一篇中我们了解到Lambda表达式表达的是一个方法。那么LambdaExpression.Compile方法自然就是将Lambda表达式的表达式树真的编译成一个.NET方法。我们看看这段代码:ParameterExpression pi = Expression.Parameter(typeof(int), i);LambdaExpression fexp = Expression.Lambda( Expression.Add(pi, Expression.Constant(1) , pi);Delegate f = fexp.Compile();已经学过第一篇的同学一眼就能看出,这个表达式树其实就是 i = i + 1。后面的Compile方法生成了一个Delegate。如果我们试着获取这个Delegate的类型会发现他是Func!实际上,这个委托所引用的方法是一个DynamicMethod编译后的结果。DynamicMethod是Reflection.Emit的强大功能,可在运行时动态创建出.NET方法来。我们可以正常地调用这个委托。Expression.Lambda这个方法还有一个泛型版重载,它可以创建如同C#自己生成一样的强类型LambdaExpression。强类型LambdaExpression的Compile就更好了,能直接生成强类型的委托。这样我们调用起来就更愉快了:ParameterExpression pi = Expression.Parameter(typeof(int), i);var fexp = Expression.LambdaFunc( Expression.Add(pi, Expression.Constant(1) , pi);var f = fexp.Compile();Console.WriteLine(f(3);如您所预料的一样,输出是“4”。有人可能要问了,我们干嘛不直接ExpressionFunc f = i = i + 1; 呢?从这个例子我们的确看不出什么优势。但是别忘了前面的Expression构造过程是完全可以用代码控制的,可以随意加入任何动态的逻辑。下面我们就来挑战一个用静态代码无法做到的任务:编写一个可以响应任何事件的响应函数。我们知道事件的响应函数大都是一个sender参数,一个EventArgs或其某子类的参数。但这只是.NET类库约定的常见用法,CLR完全没有规定Event的响应函数应该有几个参数,有没有返回值。假设没有ref或者out这样的参数(很少用于Event中),我们想用这样一个方法充当任何一个事件的响应函数:static object GeneralHandler(params object args) Console.WriteLine(您的事件发生了说); return null;但是,真正想要作为事件响应函数的那个方法必须真的是事件所需的委托类型。我们这样一个params参数的方法显然不能满足各色事件所需的独特委托。比如说我们要把这个事件挂在Button.Click事件上,而Button.Click事件想要的是这样一个委托:public delegate void RoutedEventHandler(object sender, RoutedEventArgs e);别的事件可能还需要别的委托。我们的任务就是,在运行时动态创建这一委托所需要的方法,然后调用刚才我们定义的通用响应函数,完成响应函数的挂接。这个委托的类型可以从事件的EventHandlerType中取得,然后我们再取得委托的Invoke方法即可获取它所有的参数。接下来是关键步骤,我们希望动态创建出这样一个方法来:/假设这是动态创建出来的一个方法static void RoutedEventHanler_dynamic(object sender, RoutedEventArgs e) /转嫁给刚才我们定义的GeneralHandler GeneralHandler(sender, e);这里面有一个调用params型参数的方法调用。在ExpressionTree里没有直接对应于这种调用的节点类型。我们必须还原成它编译后的底层模样,展开params参数隐式生成的数组:/假设这是动态创建出来的一个方法static void RoutedEventHanler_dynamic(object sender, RoutedEventArgs e) /转嫁给刚才我们定义的GeneralHandler /展开params调用 GeneralHandler(new Object sender, e );这个语句在C#中已经完全正确了,但是如果你直接照搬翻译成ExpressionTree,就会发现它不能编译。原因是我们的GeneralHandler接受的是object类型的参数,而调用时传递的e参数是RoutedEventArgs类型的。C#能够帮你自动增加隐式类型转换,但是手写的Expression Tree可没有这么智能,得老老实实用Expression.Convert生成类型转换的表达式节点。注意,我们这里使用的事件正好是没有返回值的,95%的事件都没有返回值,不过事件的确是允许有返回值的(而且存在这样的事件,大家可以看看AppDomain类),所以我们必须区分处理有返回值和没有返回值的情况。现在我们可以展示动态创建这一表达式树的完整代码了:(请观看注释了解代码)public static class GeneralEventHandling static object GeneralHandler(params object args) Console.WriteLine(您的事件发生了说); return null; public static void AttachGeneralHandler(object target, EventInfo targetEvent) /获得事件响应程序的委托类型 var delegateType = targetEvent.EventHandlerType; /这个委托的Invoke方法有我们所需的签名信息 MethodInfo invokeMethod

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论