自3.5版本以来,.NET以及微软的.NET语言开始支持表达式树。它们为这些语言的某个特定子集提供了eval形式的求值功能。考虑下面这个简单的Lambda表达式:
Func<int, int, int> add = (x, y) => x + y;
可以知道,上述代码最后生成一个局部函数。Lambda表达式是需要编译的代码。在C#编译器运行时,把匿名函数转换为IL代码。为了建立一个表达式树,需要对上述语法稍作修改:
Expression<Func<int, int, int>> addExpr = (x, y) => x + y;
唯一的差别是保存Lambda表达式的变量的类型。这个差别指示C#编译器生成完全不同的代码。编译器不是把表达式编译成IL代码,进行加法运算,而是自动生成一段代码,这段代码可以创建复杂的对象层级结构。对象的类型全部都是从System.Linq.Expressions.Expression派生出来的。
由于保存addExpr变量中的值是Lambda表达式的一个抽象表示,因此它不可以直接执行。但是.Net Framework可以在运行时编译一个表达式树:
Func<int, int, int> addCompiled = addExpr.Compile();
C#函数式程序设计之分析表达式
把代码看成数据这个思想的主要动理论是它允许我们在运行时分析代码。有时,这个想法的动机是了解代码的执行过程,但是,它也允许我们在运行时把代码转换为最适合于当前任务的执行格式。
表达式树使用了语法树中十分常见的一种结构。层级中包含的每个元素都派生自一个公用的基类,每个类型都需要一个派生类。同时,公用基类有一个枚举类型字段,在进行任何类型转换之前,利用这个字段进行类型检查。
在实际应用中,分析表达式树的目的是把表达式树转换为不同的格式,但是它们表示同一个或同类表达式。考虑下面这个LINQ表达式:
var peopleWithInTheirName = from p in people where p.Name.Contains("i") select p;
这个简短的查询要在people集合中查找所有由Name属性返回的字符串中有i的元素。对于LINQ,C#编译器把像这个例子的查询表达式转换为一系列的方法调用,把where字句转换为Lambda表达式:
p => p.Name.Contains("i");
这个表达式接受一个对象p,然后返回针对这个对象的某个条件是否成立。这种类型的表达式被称为谓词。.NET Framework专门为这些表达式预定义了一个委托类型,即Predicate<T>:
Predicate<Person> nameContainsI = p => p.Name.Contains("i");
Predicate<T>在功能上等效于Func<T , bool>:接受类型为T的一个项为参数,返回一个bool值表示这个给定的T参数是否满足某个条件。
下面是一个用递归算法求阶乘的简单函数:
static int Fact(int x) { int result = 1; for (int i = 2 ; i <= x; i++) result *= i; return result; }
如果使用Lambda表达式:
Func<int, int> Fact = x => { int result = 1; for (int i = 2; i <= x; i++) result *= i; return result; };
表达式树的核心是运行时处理代码——不管是需要理解和分析代码,或者动态生成代码。