大家可能都知道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) => expression |
Lambda表达式最重要的特色是它可以引入一批参数,这批参数可以在函数体表达式中使用。基于这种特色,我们就可以创建出带自定义变量的表达式树,而这些自定义变量就表示成Lambda表达式的参数:
ParameterExpression expA = Expression.Parameter(typeof(double), "a"); //参数a MethodCallExpression 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<TDelegate>类型。我们用它来表示强类型的LambdaExpression。现在我们就要引入 C#、VB真正表达式和表达式树之间的桥梁——表达式树字面量(Expression Tree Literals),可以自动从Lambda表达式生成它的表达式树:
Expression<Func<double, double>> exp = a => Math.Sin(a); |
注意这个赋值语句,左侧是一个强类型的LambdaExpression:Expression<Func<double, double>>,右侧是一个真正的C#语法的Lambda表达式。C#的编译器在这种情况下就能自动为你生成右侧Lambda表达式的表达 式树。也就是说,这个exp和我们刚才手工生成Lambda表达式树基本是一样的。注意,这种特殊的语法仅能从Lambda表达式获得表达式树。别的表达 式是不能自动生成表达式树的。但是一旦我们获得了Lambda表达式,就可以直接从它的子节点获得内部表达式了。这是一个非常有用的语法,要深刻理解它的 作用。
需要注意的是,这里的委托类型Func<double, double>有双重作用,首先它限定生成的表达式树是一个接受double,并返回double的一元Lambda函数;其次这个委托可以直接用 在Lambda表达式树的编译当中,可在C#作强类型处理。我们后面谈到表达式树的编译时再详细的讨论这个问题。
表达式树的意义:数据化的表达式
我们现在已经能够用两种方式创建表达式树——用Expression的节点组合或者直接从C#、VB的Lambda表达式生成。不管使用的是那种方 法,最后我们得到的是一个内存中树状结构的数据。如果我们愿意,可以将它还原成文本源代码的表达式或者序列化到字符串里。注意,如果是C#的表达式本身, 我们是没法对它进行输出或者序列化的,它只存在于编译之前的源文件里。现在的表达式树已经成为了一种数据,而不在是语言中的表达式。我们可以在运行的时候 处理这一数据,精确了解其内在逻辑;将它传递给其他的程序或者再次编译成为可以执行的代码。这就可以总结为表达式树的三大基本用途:
- 运行时分析表达式的逻辑
- 序列化或者传输表达式
- 重新编译成可执行的代码
在下一篇中,我们将着重介绍这三者在实际开发中的用途。
习题
还有习题?别担心,你可以将下列问题当做上机实践的素材,以便很快地理解本次学到的内容。
第一题:画出下列表达式的表达式树。一开始,您很可能不知道某些操作其实也是表达式(比如取数组的运算符a[2]),不过没有关系,后面的习题将帮你验证这一点。
-a
a + b * 2
Math.Sin(x) + Math.Cos(y)
new StringBuilder(“Hello”)
new int[] { a, b, a + b}
a[i – 1] * i
a.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#编译器有时会使用一些作弊手法,聪明的你应该能找到绕过的手段……
(待续)