深入Lambda表达式

来源:互联网 发布:支持mac的大型网游 编辑:程序博客网 时间:2024/05/12 14:27

 在前一篇文章中,我简要介绍了关于Lambda的一些基本知识,有了这些认识以后,我们可以再向Lambda表达式深入一点。在这篇文章里,我主要想对表达式树和表达式与Expression对象的对应关系做一个简要的说明。

首先,表达式树的概念我们必须先有一个大致的了解,MSDN里是这样解释的:表达式目录树以数据形式表示语言级别代码。数据存储在树形结构中。表达式目录树中的每个节点都表示一个表达式,例如一个方法调用或诸如 x < y 的二元运算。(全文请点这里

这样的解释可能还不够清楚,但有一个概念已经说得非常清楚了,就是表达式树中的每一个节点都是一个表达式。那么,组成表达式的最基本要素是什么呢,简单的说就是表达式的形式和参数,表达式的形式用来描述表达要做什么事,例如二元运算(Binary)、对象成员访问(Member)、常量表达式(Constant)等等,而参数就是表达式要完成这件事所必须的数据,每一个参数又都是一个独立的表达式,它们构成了当前节点的子节点。下面我们以实例来说明,参考以下例子:

Expression<Func<string, bool>> expr = str => str != null && str.Length > 10;

既然是树型结构,必须要有一个根结点。其实,我们可以把表达式最终返回的值看作是这个树的根结点,这样再往下分析就会容易些了,为了方便表述把根结点记为N。上例中要返回的是一个布尔值,而该布尔值最终是由一个逻辑与操作符(&&)运算得来,这是一个典型的二元运算,它的参数就是运算符两边的表达式,这样就可以这两个表达式看作是该结点下面的两个子结点,分别记作N1和N2。

接下来看N1结点上的表达式str != null,这是一个进行非空值测试的二元运算,不等号左边的是变量str,该变量的值由外部传入,是一个参数表达式;右边是一个表示空值的常量null,是一个常量表达式。由此可以看出,N1下面可以有两个子结点,分别记为N11和N12,而由于参数表达式和常量表达式已经无法再细分了,因此N11和N12不再具有下级结点。

N2结点的表达式str.Length > 10与N1结点类似,左右两边分别记为N21和N22。与N1不同的是,N21表达式str.Length是一个对对象成员进行访问的表达式,它还可以再进行拆分,可把需要访问成员的对象看作是N21的子节点,记作N211,很明显这里要访问的是str变量,因此N211是一个参数表达式。仔细的读者一定已经发现了,N211和N11其实表示的是同一个表达式,的确是这样的。

经过以上的分析,我们已经把上面例子中的表达式拆分成了表达式树,下面这张图可以非常明确的表示出这个表达式树的结构。

表达式树示例

那么,了解了这个表达式树之后,我们能做些什么呢?答案很简单,一方面可以增加对Lambda表达式的理解,更重要的是可以动态地构建Lambda表达式了。关于动态构建表达式树,我将会在下一次的文章中做进一步的介绍,而在本文接下来的篇幅中,我要再说明一个基础知识,也就是各种表达式与CLR中定义的表达式之间的对应关系。

由以上的说明可以看出,要将一个表达式拆分成表达式树,首先要了解表达式的形式。在CLR中,System.Linq.Expressions命名空间中已经定义了在3.5版中所要用到的所有表达式的类型,它们都是Expression类的派生类,每一种派生类都代表了一种表达式的形式。从MSDN中对Expression类的说明中看出,Expression类的派生类共有14种,如果要以编程的方式创建这些派生类,可以使用Expression类提供的工厂方法来创建表达式。在下表中按名称顺序对这派生类及使用场合进行简要的说明。

派生类名称创建方法适用场合
BinaryExpressionAdd, Multiply, Divide, And, Or等等表示二元运算。这是运用最广泛的一种表达式,所有二元运算均可使用这种派生类来表示,上述例子中也多处使用了此种类型的表达式。
ConditionalExpressionCondition表示条件运算符,当条件满足时返回表示“真”的值,否则返回表示“假”的值,C#里的代码示例:i > 10 ? "Yes" : "No"。
ConstantExpressionConstant用于表示一个常量。
InvocationExpressionInvoke用于表示对一个委托或Lambda表达式的调用。
LambdaExpressionLambda表示一个Lambda表达式,通常情况下都会在最后用这个方法来生成我们期望的Lambda表达式。
MemberExpressionProperty, Field, PropertyOrField表示对某个对象的属性或字段的访问,上面的例子中N21节点即属于此类型。
MethodCallExpressionCall, ArrayIndex表示在某个对象上调用指定的方法。例如要测试一个字符串是否以“123”开头,str.StartsWith("123")。
NewExpressionNew表示调用构造函数初始化一个对象的新实例。例如:DateTime datetime = new DateTime()。
NewArrayExpressionNewArrayBounds, NewArrayInit表示创建并初始化一个数组,分为初始化数组大小和初始化数组元素两种方式。
如果在创建数组时仅需要指定数组的大小,例如int[] array = new int[10],可使用NewArrayBounds方法来创建。
如果要直接初始化数组的值,例如int[] array = new int[] { 1, 2, 3, 4 },可使用NewArrayInit方法来创建。
MemberInitExpressionMemberInit表示调用构造函数初始化一个对象后再初始化其成员的表达式,参考以下语句:
Person obj = new Person { Name = "effun", Gender = Male, Age = 32 };
ListInitExpressionListInit用于集合初始化的表达式,这与使用NewArrayInit的情况非常相似,但它可以应用在更复杂的场合,例如对字典对象的初始化,请参考以下示例:
Dictionary<int, StudentName> students = new Dictionary<int, StudentName>()
{
    { 111, new StudentName {FirstName="Sachin", LastName="Karnik", ID=211}},
    { 112, new StudentName {FirstName="Dina", LastName="Salimzianova", ID=317, }},
    { 113, new StudentName {FirstName="Andy", LastName="Ruth", ID=198, }}
};
ParameterExpressionParameter表示一个已命名的参数,通常情况下指传入Lambda表达式的参数,如上例中的N11和N211结点。
TypeBinaryExpressionTypeIs表示一个表达式和一种类型的二元运算,目前唯一的应用就是测试某个变量是否为指定的类型,例如:obj is String。
UnaryExpressionArrayLength, Negate, Convert等等表示一个一元运算,主要的运算有计算数组长度、负数、类型转换和自增等等。例如a = -a,a++等。

以上对CLR中各种表达式类型进行了一次概要说明,具体的使用方法请参考MSDN中对各工厂方法的帮助,在这里就不详细说明了。在了解了这些表达式的用途之后,我们就对如何拆分表达式树,以及后面要说的动态构建表达式有了一定的基础。从实用的角度来考虑,如果要动态构建的表达式仅用于条件判断,那么上述的14种表达式并不会全部用到,常用的是BinaryExpression、MemberExpression、MethodCallExpression、ParameterExpression、ConstantExpression。