概述
表达式树(Expression Tree):树形数据结构表示代码,以表示逻辑运算,以便可以在运行时访问逻辑运算的结构。
在 C# 中,表达式树是一种数据结构,它表示了一个 lambda 表达式或者一个 LINQ 查询的代码结构。表达式树可以在运行时动态创建、修改和执行。它通常被用于将查询表达式转换为 SQL 语句或者其他目标语言的代码。
C# 中的表达式树主要由两个类组成:Expression 和 Expression。其中 Expression 是一个抽象类,表示表达式树的节点。而 Expression 则是一个泛型类,表示带有返回值的表达式树节点。
下面是一个简单的示例,说明了如何使用表达式树来创建一个 lambda 表达式:
using System;
using System.Linq.Expressions;
class Program
{
static void Main()
{
// 创建一个参数
ParameterExpression x = Expression.Parameter(typeof(int), "x");
// 创建一个 lambda 表达式
//Expression.Multiply(x, x)的两个参数表示要相乘的两个数
Expression<Func<int, int>> square = Expression.Lambda<Func<int, int>>(
Expression.Multiply(x, x), new ParameterExpression[] { x });
// 编译并调用这个 lambda 表达式
Func<int, int> squareFunc = square.Compile();
Console.WriteLine(squareFunc(5)); // 输出 25
}
}
在上面的示例中,首先定义了一个参数 x,然后创建了一个 lambda 表达式 square,它表示计算 x * x 的函数。最后,编译并调用这个 lambda 表达式,输出结果为 25。
在表达式树中,每个操作符都对应着一个表达式树节点类型。例如,上面的示例中使用了 Expression.Multiply 方法来创建一个乘法运算符的节点。
除了基本的操作符之外,表达式树还支持一些特殊的节点类型,例如 MethodCallExpression 和 MemberExpression。这些节点可以代表方法调用和成员访问等操作。
总的来说,C# 中的表达式树是一种非常强大的工具,它可以帮助我们动态地创建和执行代码,并且可以方便地将查询表达式与其他目标语言的代码进行转换。
示例,使用 Expression.Multiply 方法创建一个相乘的表达式树节点示例,
首先创建了两个常量表达式 first 和 second,表示要相乘的两个数。然后使用 Expression.Multiply 方法创建一个相乘的表达式树节点 multiply,将这个节点传递给 Lambda 方法创建 lambda 表达式,并且使用 Compile 方法编译这个 lambda 表达式。最后调用这个编译好的函数,输出结果为 6。
using System;
using System.Linq.Expressions;
class Program
{
static void Main()
{
// 创建第一个参数
Expression first = Expression.Constant(2);
// 创建第二个参数
Expression second = Expression.Constant(3);
// 创建相乘的表达式树节点
Expression multiply = Expression.Multiply(first, second);
// 编译并执行表达式树节点
Func<int> func = Expression.Lambda<Func<int>>(multiply).Compile();
Console.WriteLine(func()); // 输出 6
}
}
常用方法
除了 Expression.Multiply 方法之外,Expression 类还提供了许多其他常用的方法,例如:
Expression.Add:创建表示两个数相加的表达式树节点。Expression.Subtract:创建表示两个数相减的表达式树节点。Expression.Constant:创建一个常量表达式。Expression.Parameter:创建一个参数表达式。Expression.Call:创建表示方法调用的表达式树节点。Expression.MemberAccess:创建表示成员访问的表达式树节点。
下面是一个示例,演示了如何使用这些方法来创建一个复杂的表达式树:
using System;
using System.Linq.Expressions;
class Program
{
static void Main()
{
// 创建第一个参数
ParameterExpression x = Expression.Parameter(typeof(int), "x");
// 创建第二个参数
ParameterExpression y = Expression.Parameter(typeof(int), "y");
// 构造表达式树:(x + y) * 2 + 1
Expression result = Expression.Add(
Expression.Multiply(
Expression.Add(x, y),
Expression.Constant(2)
),
Expression.Constant(1)
);
// 编译并执行表达式树节点
Func<int, int, int> func = Expression.Lambda<Func<int, int, int>>(result, x, y).Compile();
Console.WriteLine(func(2, 3)); // 输出 11
}
}
在上面的示例中,首先创建了两个参数 x 和 y,然后构造了一个复杂的表达式树 (x + y) * 2 + 1。最后编译这个表达式树,并且调用得到的函数,输出结果为 11。
总的来说,Expression 类是 C# 中表达式树的核心类之一,它提供了许多方法,用于创建和修改表达式树节点。通过灵活运用这些方法,我们可以创建出非常复杂的表达式树,实现各种动态代码生成的需求。
LambdaExpression
在Lambda表达式中,如果你想要获取所有的参数信息,就需要用到 Parameters方法。
body属性
在 C# 中,Lambda 表达式可以被用于创建匿名方法。其中 LambdaExpression 类型表示一个 Lambda 表达式,该类型有一个叫做 Body 的成员方法,它返回一个表示 Lambda 表达式主体的表达式树。
在下面的示例中,我们定义了一个接收两个 int 类型参数并返回它们之和的匿名函数:
Func<int, int, int> sum = (x, y) => x + y;
我们可以使用 sum 函数对两个数字求和,如下所示:
int result = sum(1, 2); // result = 3
但是,我们也可以使用 LambdaExpression.Body 方法访问 sum 匿名函数的主体(body),从而获取关于 Lambda 表达式的更多信息。
var lambdaExpr = (LambdaExpression)sum;
string bodyStr = lambdaExpr.Body.ToString(); // "x + y"
以上代码会将 sum 函数转换为 LambdaExpression 类型,并使用 ToString 方法来获取其主体的字符串表示形式。
除了 ToString 方法之外,LambdaExpression.Body 方法返回一个 System.Linq.Expressions.Expression 类型的对象,该对象代表着 Lambda 表达式主体的表达式树。我们可以使用这个对象来执行一些高级操作,例如将 Lambda 表达式重构为不同的形式或者解析它以获得进一步信息。
Parameters属性
Parameters 方法返回一个 IEnumerable 类型的对象(ReadOnlyCollection),在这个对象中存储了当前Lambda表达式所包含的所有参数的信息,每个元素是ParameterExpression对象。
下面是一个示例代码,演示了如何使用 Parameters方法:
using System;
using System.Linq.Expressions;
class Program {
static void Main(string[] args) {
Expression<Func<int, int, int>> lambda = (x, y) => x * y;
Console.WriteLine("Parameters of lambda expression:");
foreach (var parameter in lambda.Parameters) {
Console.WriteLine(parameter.Name);//x y
Console.WriteLine(parameter.Type);//System.Int32 System.Int32
}
}
}
在这个示例代码中,我们创建了一个Lambda表达式,并使用 Parameters方法遍历了它的所有参数。当程序运行时,输出会像这样:
Parameters of lambda expression:
x
System.Int32
y
System.Int32
将两个lambdaExpression组合
要将两个 Expression<Func<LadleRoute, bool>> 谓词进行与操作,可以使用 Expression.AndAlso() 方法。
Expression<Func<LadleRoute, bool>> expr1 = r => r.Weight > 1000;
Expression<Func<LadleRoute, bool>> expr2 = r => r.Length < 500;
//将x使用下面的方法创建,执行GetListAsync方法仍然报错
//ParameterExpression x = Expression.Parameter(typeof(LadleRoute), "x");
var andExpr = Expression.AndAlso(expr1.Body, expr2.Body);
//明确finalExpr的具体类型仍然报错
var finalExpr = Expression.Lambda<Func<LadleRoute, bool>>(andExpr, expr1.Parameters);
//这个表达式调用Compile方法时就会报错
//variable 'x' of type 'Arim.Poi.LadleService.Ladles.LadleRoute' referenced from scope '', but it is not defined
//finalPredicate.Compile();
// 使用finalExpr作为你的新谓词
return await _ladleRouteRepository.GetListAsync(finalExpr);
在上面的代码中,我们创建了两个 Expression<Func<LadleRoute, bool>> 谓词 expr1 和 expr2,并通过调用 AndAlso() 方法将它们进行了 AND 操作。然后,我们通过 Expression.Lambda() 构造了一个新的 Lambda 表达式,并用于最终的查询谓词中,该谓词可用于 EF Core 查询或 LINQ to SQL 查询等多种地方。
但是,经过实际操作,发现组合后是这样的形式(这样的形式看起来也正常)
{x => ((x.LadleStatus == LadleStateType.Empty.Name) AndAlso ((((x.EventTime != null) AndAlso (x.EventTime >= value(Arim.Poi.LadleService.Ladles.LadleRouteAppService+<>c__DisplayClass25_0).startTime)) AndAlso (x.EventTime <= value(Arim.Poi.LadleService.Ladles.LadleRouteAppService+<>c__DisplayClass25_0).endTime)) AndAlso x.LadleNo.StartsWith(value(Arim.Poi.LadleService.Ladles.LadleRouteAppService+<>c__DisplayClass25_0).startChar)))}
不能在GetList方法中直接使用,问题未解决
表达式树和委托
用Expression<TDelegate>类型保存表达式树。Expression对象储存了运算逻辑(而普通委托则没有),它把运算逻辑保存成抽象语法树(AST),可以在运行时动态获取运算逻辑
//从Lambda表达式生成表达式树
Expression<Func<Book,bool>> e1=b=>b.Price>5
注意,无法将带方法体的语句转换成表达式树
//在设断点调试时可以看到表达式树
Expression<Func<Book,double>> e2=(b1,b2)=>b1.Price+b2.Price
//直接写一个Func委托看不到表达式结构,只是一个普通委托
Func<Book,double> e2=(b1,b2)=>b1.Price+b2.Price
表达式树的组成
- ParameterExpression:参数
- BinaryExpression:二元运算符
- MethodCallExpression:方法调用
- ConstantExpression:常量
表达式树的创建
通过代码来动态创建表达式树要求开发者精通表达式树的结构,甚至还要了解CLR底层的机制。不过可以用ExpressionTreeToString来简化动态构造表达式树的代码
ParameterExpression、BinaryExpression、MethodCallExpression、ConstantExpression等类几乎都没有提供构造方法,而且所有属性也几乎都是只读的,因此我们一般不会直接创建这些类的实例,而是调用Expression类的Parameter、MakeBinary、Call、Constant等静态方法来生成,这些静态方法我们一般称作创建表达式树的工厂方法,而属性则通过方法参数类设置
//创建常量节点
Expression.Constant(5);
有哪些工厂方法
- Add:加法
- AndAlso:短路与运算
- OrElse:||短路或运算
- Parameter:表达式的参数
- ArrayAccess:数组元素访问
- Call:方法访问
- Condition:三元条件运算符
- Constant:常量表达式
- Convert:类型转换
- GreatherThan、LessThan:大于、小于运算符
- GreatherThanOrEqual:>=运算符
- MakeBinary:创建二元运算
- NotEqual:!=运算符
查看表达式树的方法
方法一:通过vs调试
Visual Studio中调试程序,然后用【快速监视】的方式查看变量e的值,展开Raw View
方法二:通过代码查看
//Nuget安装包,它定义了Expression的扩展方法
ExpressionTreeToString
通过Expression的ToString扩展方法查看
Expression<Func<Book, bool>> e = b => b.AuthorName.Contains("xxx") || b.Price > 5;
Console.WriteLine(e.ToString("Object notation","C#"));
整个表达式树是一个“或”(OrElse)类型的节点,左节点(Left)是b.Price>5表达式,右节点(Right)是b.AuthorName.Contains("xxx")表达式。而b.Price>5这个表达式又是一个“大于”(GreaterThan)类型的节点,左节点(Left)是b.Price,右节点(Right)是5
动态构建表达式树
注意:尽量避免使用动态构建表达式树,代码复杂,而且易读性差,维护起来头疼。在编写不特定于某个实体的通用框架时才适合使用
为什么要动态构建表达式树
只有通过代码动态构建表达式树才能更好的发挥表达式树的能力。动态构建表达式树最有价值的地方就是运行时根据条件的不同生成不同的表达式树。
比如下面的场景
- 查询时,采用不同查询条件,可以在运行时生成表达式
- 针对某个实体,动态构建查询条件时,利用IQueryable的延迟执行特性来动态构造
步骤
1、用ExpressionTreeToString("Factory methods","C#"))输出类似于工厂方法生成这个表达式树的代码
输出的所有代码都是对工厂方法的调用,不过调用工厂方法的时候都省略了Expression类。手动添加Expression或者用using static System.Linq.Expressions.Expression
2、微调生成的代码