这节来讲一下C#中的表达式树(又称表达式目录树、Expression)。

什么是表达式树?

 表达式树是一种C#中的数据结构,它以树的形式表示某些代码内部的结构。每个节点是一种称为表达式的C#对象,例如二元运算,方法调用,常量等。这种数据结构主要用于LINQ查询的内部机制和动态编程。在C#中,表达式树使在编译时表达式的结构和操作被保留下来,而不是像通常的.net代码那样被直接编译成IL。这使得你可以在运行时操作这些表达式或将它们转换成其他形式。例如,你可以将一个表达式树转换为可重用的Lambda表达式,或者用于创建动态查询。或者,你可以遍历表达式树来读取和解析表达式的结构。这种技术是.NET Framework中LINQ的基础,特别是在使用LINQ to SQL和LINQ to Entities时,因为它允许在运行时将LINQ查询表达式转换为SQL查询。

表达式树、lambda、委托三者区别

表达式树、lambda、委托看似很像,写代码的时候也经常配套使用,那三者有什么区别呢?

1. 委托:在C#中,委托(Delegate)是一种类型安全的函数指针,它定义了可以代表的方法的类型。这允许你将方法作为参数传递,或者将方法存储在变量中。它是.NET事件处理的基础。

2. lambda表达式:lambda表达式是创建委托或表达式树类型的一种便捷方式。通过使用lambda表达式,你可以编写局部函数,这些函数可以在表达式或语句的上下文中使用。lambda表达式是匿名的,它们不具有特定的名称。

3. 表达式树:表达式树是一种特殊的数据结构,主要用于表示和处理代码以数据的形式。它们常常用于创建动态查询和解析、处理和执行命令模式。表达式树可以从lambda表达式创建,然后可以被编译并执行。总的来说,lambda表达式是创建表达式树和委托实例的一种方式,委托是一种可以引用方法的类型,而表达式树则提供了一种灵活处理代码的方式,使得你可以在运行时操作和执行代码。

代码演示

public class Program
    {
        public static void Main()
        {
            // 参数表达式
            ParameterExpression numParam = Expression.Parameter(typeof(int), "num");
            // 常数表达式
            ConstantExpression five = Expression.Constant(5, typeof(int));
            // 比较表达式: num > 5
            BinaryExpression numGreaterThanFive = Expression.GreaterThan(numParam, five);
            // Lambda表达式
            LambdaExpression lambda1 = Expression.Lambda(numGreaterThanFive, new ParameterExpression[] { numParam }); 
            // 编译并执行
            Console.WriteLine("num > 5: " + ((Func<int, bool>)lambda1.Compile())(6)); // 输出: num > 5: True
        }
    }

在这个示例中,我们创建了一个表达式树来表示 "num > 5" 运算。然后,我们把这个表达式树转换为一个Lambda表达式,并且编译并运行这个Lambda表达式,输出其结果。

反射与表达式树

在.NET中,表达式树和反射都可以用来在运行时动态地生成和执行代码。然而,表达式树提供了一种在执行效率和代码清晰度方面更优的选择。

反射是.NET框架提供的一种功能,它允许我们在运行时获取类型的信息,创建对象,调用方法,获取和设置字段/属性的值等。然而,反射的缺点在于它的执行效率不高,因为它需要在运行时解析类型信息。此外,反射代码往往难以阅读和维护。

而表达式树实际上是一个数据结构,它以树形式表示代码。我们可以创建和修改表达式树,然后将其编译为委托并执行。表达式树的主要优点在于它们可以在运行时生成和编译,从而提供了比反射更高的执行效率。此外,表达式树的代码通常比反射代码更清晰,更易于理解。例如,假设我们需要动态地调用一个对象的方法。使用反射,我们需要获取类型的信息,查找方法,创建参数,并调用方法。使用表达式树,我们可以创建一个表示该方法调用的表达式树,然后将其编译为委托并执行。因此,虽然表达式树和反射都可以在运行时动态地生成和执行代码,但在很多情况下,表达式树提供了一种效率更高、代码更清晰的选择。

下面通过一个例子来比较一下如何通过反射和表达式树访问对象的属性。

有如下一个类:

public class Person
    {
        public string Name { get; set; }
    }

使用反射读取其Name属性:

Person person = new Person { Name = "川建国" };Type type = typeof(Person);
PropertyInfo propertyInfo = type.GetProperty("Name");string name = (string)propertyInfo.GetValue(person);
Console.WriteLine(name); // 输出: 川建国

使用表达式树形式读取其Name属性:

Person person = new Person { Name = "川建国" };
ParameterExpression objExp = Expression.Parameter(typeof(Person), "p");
MemberExpression propertyExp = Expression.Property(objExp, "Name");
LambdaExpression lambdaExp = Expression.Lambda(propertyExp, objExp);
Func<Person, string> getPersonName = (Func<Person, string>)lambdaExp.Compile();
string name = getPersonName(person);
Console.WriteLine(name); // 输出: 川建国

  

我们可以看到,虽然表达式树的代码看起来更复杂一些,但实际上它运行得更快,特别是在需要重复执行的情况下,因为编译过的委托可以重复使用,而反射每次都需要重新解析类型信息和方法信息。

最后,概述一下表达式的特点

1. 表达式树是代码的数据结构表示:与直接编写的代码不同,表达式树是代码的数据结构表示。它让你可以在运行时检查和操作数据,就像操作其他数据结构一样。 

2. 表达式树可以被动态生成:这是表达式树的一个重要特性,你可以在运行时动态创建和修改表达式树。这对于需要动态生成和执行代码的场景(例如,LINQ提供者)非常有用。

3. 表达式树可以被编译并执行:表达式树不仅可以表示代码,还可以被编译并执行。这使得表达式树比反射有更好的性能,因为反射需要在运行时解析类型和方法信息,而表达式树在编译后就可以直接执行。 

4. 表达式树可以用于创建LINQ查询:LINQ查询实际上就是表达式树。当你写一个LINQ查询时,编译器实际上是在后台创建一个表达式树。这个表达式树然后可以被LINQ提供者(如Entity Framework)用来生成和执行相应的SQL查询。 

5. 表达式树可以用于序列化和反序列化表达式:由于表达式树是代码的数据结构表示,你可以将其序列化为二进制或文本格式,然后在另一个上下文(甚至在另一个进程或机器)中反序列化并执行。这对于远程过程调用(RPC)和分布式计算等场景非常有用。