手把手构建 C# 表达式树

1. 准备工作

创建 PersonPerson.cs

public class Person
{
    public string Name { get; set; } = null!;
	public int Age { get; set; }
}

拿到表达式 p => p.Age > 18,我们用上一篇文章的思路,把它画成一个简单的表达式树:

在这里插入图片描述
图 1

表达式各元素与表达式树各节点的对应关系如下图:

在这里插入图片描述
图 2

2. 构建表达式树

  1. 首先构建 2 号节点,这个节点是个参数表达式(ParameterExpression):
ParameterExpression parameterExpr = Expression.Parameter(typeof(Person), "p");

现在我们构建完成了这部分:

在这里插入图片描述
图 3
  1. 构建 4、6、7 号节点的表达式,因为这三个节点的目的是取出参数 p 的 Age 成员属性,所以三个节点可以构建成一个成员表达式(MemberExpression),可以使用上面构建好的 2 号 p 参数节点。:
MemberExpression memberExpr = Expression.PropertyOrField(parameterExpr, "Age");

现在我们构建完成了这部分,绿色框表示前面已经构建完成的部分:

在这里插入图片描述
图 4
  1. 构建 5 号节点的表达式。该节点是一个常量表达式(ConstantExpression):
ConstantExpression constantExpr = Expression.Constant(18, typeof(int));

Expression.Constant() 是构建常量表达式的方法。

现在构建完了这部分:

在这里插入图片描述
图 5
  1. 通过前面构建的 4、6、7 号成员表达式和 5 号常量表达式,构建 3 号节点的大于号的二元运算符表达式(BinaryExpression),因为 > 是二元运算:
BinaryExpression greaterThanExpr = Expression.MakeBinary(ExpressionType.GreaterThan, memberExpr, constantExpr);

Expression.MakeBinary() 是构建二元运算符表达式的方法,第一个参数是二元运算符表达式的类型,这里我们是 >,所以参数就是 ExpressionType.GreaterThan,第 2、3 个参数是 > 左右两侧的参数,所以顺序不能反了,如果反了,表达式就会变成 18 > p.Age

现在构建完了这部分:

在这里插入图片描述
图 6
  1. 最后将第 1 步构建的 p 的参数表达式和第 4 步构建的 p.Age > 18 的二元运算表达式中间连接上 => 符号,组成一个 lambda 表达式即可:
Expression<Func<Person, bool>> finalExpr = Expression.Lambda<Func<Person, bool>>(greaterThanExpr, parameterExpr);

Expression.Lambda() 的第一个参数是方法体,也就是 p.Age > 18greaterThanExpr 表达式,第二个参数是方法参数 p,也就是第 1 步构建的 parameterExpr

因为我们要构建的表达式 p => p.Age > 18 是一个接受 Person 类型参数,返回 bool 类型值的函数,也就是 Func<Person, bool> 委托,所以 Expression.Lambda() 方法的泛型参数以及最终返回的 Expression 的泛型类型都是 Func<Person, bool>

现在构建完成了,最终得到了 finalExpr

3. 使用表达式树

我们编写一些代码来测试使用这个表达式树:

var personsList = new List<Person>()
{
    new Person
    {
	    Name = "Zhang Three",
	    Age = 8
    },
    new Person
    {
	    Name = "Li Four",
	    Age = 19
    },
    new Person
    {
	    Name = "Wang Wu",
	    Age = 10086
    }
};
var result = personsList.Where(finalExpr.Compile()).ToList();
foreach (var person in result)
{
    Console.WriteLine(person);
}

运行结果:

Name: Li Four, Age: 19
Name: Wang Wu, Age: 10086

4. 是多此一举吗?

在这里我们手动调用了 Compile() 方法将表达式编译了,似乎是多此一举,不如直接在 Where() 方法中写 lambda 表达式,就像这样:var result = personsList.Where(p => p.Age > 18).ToList();

我们这里调用的是 IEnumerableWhere() 方法。其实 IQueryableWhere() 等方法支持直接使用表达式,不需要手动调用 Compile() 方法编译,也就是说可以配合 EF Core 使用我们自己构建的表达式,而且会发挥奇效。

我接下来会写两篇把表达式树应用在 EF Core 的示例的文章,并配有代码:

  1. 展示如何动态构建表达式树来简化和语义化值对象(DDD 中的概念)的比较
  2. 如何为每一个实体类的属性(例如软删除的属性)设置过滤器,而不是手动在每一个实体类的 IEntityTypeConfiguration 类中一个一个设置

如何使用 CSharp 表达式树?

本篇博客介绍了什么是表达式树,Func<T>Expression<TDelegate> 的区别,以及如何手动构建 Expression

1. 什么是表达式树

表达式树(expression tree)是用树型的数据结构来表示代码的运算逻辑。你的代码也是你可以用来操作的数据数据也可以变成代码。当你拿到数据时(比如从数据库拿到数据),你可以通过这些数据创建出代码。这就是表达式树。

说到这里,你可能完全不理解,我们慢慢探索。首先看一下表达式树的图。

1 + 2 这个表达式的表达式树:

在这里插入图片描述
图 1

假设我们先序遍历这棵树,首先我们拿到 +,我们知道这是要进行一个加法操作,然后我们拿到 1,再拿到 2,我们就知道是要运算 1 和 2 相加的结果。

1 + 2 * 3 的表达式树(注意,乘的优先级高于加):

在这里插入图片描述
图 2

1 与另一个表达式子树的结果相加,表达式子树是 23* 运算。

顺便看一下这些表达式在 c#中的语法树。

void Main()
{
	int a = 1 + 2;
}
在这里插入图片描述
图 3

上图是 a = 1 + 2 的语法树,红框中是 1 + 2 的语法树,变成图的话就是这样:

在这里插入图片描述
图 4

跟前面我们绘制的表达式树的图是基本相同的,很容易理解。

1 + 2 * 3 也是相似的:

void Main()
{
	int a = 1 + 2 * 3;
}
在这里插入图片描述
图 5

2. Func<T>Expression<TDelegate>

表达式树在 .NET 中就是 Expression<TDelegate> 类型。C# 编译器可以从 lambda 表达式生成 Expression<TDelegate> 类型的变量。

Expression<TDelegate> 也可以直接编译成 TDelegate 类型的委托对象,我们用 Func<T> 来举个例子。

Func<T> 这个 Func 委托实际上就是一个方法,该类型的变量可以直接当方法调用。

通过代码更容易理解:

void Main()
{
	Func<int> one = () => 1;
	Console.WriteLine(one()); // 1
	Console.WriteLine(one.GetType()); // System.Func`1[System.Int32]
        
    // 从 lambda 表达式生成 Expression<TDelegate> 类型的变量
	Expression<Func<int>> oneExpression = () => 1;
	Console.WriteLine(oneExpression.Compile()()); // 1
	Console.WriteLine(oneExpression.Compile().Invoke()); // 1
	Console.WriteLine(oneExpression.GetType()); // System.Linq.Expressions.Expression0`1[System.Func`1[System.Int32]]
	Console.WriteLine(oneExpression.Compile().GetType()); // System.Func`1[System.Int32]
}

Expression<Func<int>> 类型的 oneExpression 变量调用了 Compile 方法后,得到的结果的类型就是 Func<int> 了。

Func<int> 对象是一个方法,而 Expression<Func<int>> 对象则存储了方法的信息,可以通过这些信息来编译(compile)出这个方法。

更进一步说,Expression 对象以抽象语法树的形式存储了运算逻辑,我们可以在运行时动态分析运算逻辑。

图 6 就是以结构化的方式查看 oneExpression 这个语法树的结构。

在这里插入图片描述
图 6

可能现在看这张图还有些不理解,下面尝试手动构建一个语法树,来理解这些图。

3. 手动构建 Expression

举一个简单的例子:

void Main()
{
	var selectProperty = "age";
	
	var person = new Person 
	{
		Name = "Kit Lau",
		Age = 18
	};
	
	if (selectProperty == "name")
	{
		Console.WriteLine(person.Name);
	}
	else if (selectProperty == "age")
	{
		Console.WriteLine(person.Age);
	}
	else 
	{
		throw new Exception($"class Person has no property named {selectProperty}");
	}
}
class Person
{
	public string Name { get; set; }
	public int Age { get; set; }
}

这段代码实现的功能是:假设 selectProperty 是用户手动选择或者输入的一个代表属性名的字符串,通过它的值来让程序打印 Person 类中的属性名与用户输入的 selectProperty 相同的属性的值。例如用户输入的 selectProperty 的值是字符串 "Age""age",就在控制台打印 person 变量的 Age 属性的值。

这段程序里我们通过 if 语句来判断应该打印哪个属性的值,但是我们在 if 语句里硬编码了 "name""age" 两个字符串。如果后面有需求修改了 Person 的属性名,把 Name 改为了 EnglishName,开发者无法通过 Visual Studio 等 IDE 的查找引用功能来直接找到这部分代码,只能遍历所有带 "name" 字符串的代码,这种代码在一个项目中何其多。这种硬编码的方式会使这份代码不可维护。

下面我们改一下代码,通过创建 Expression 来实现相同的功能。

首先我们分析一下这个表达式树应该是什么样子。先写如下代码:

void Main()
{
	var person = new Person();
	Expression<Func<Person, object>> exp = p => p.Name;
	Console.WriteLine(exp);
}

看一下表达式树:

在这里插入图片描述
图 7

Expression 画成树的形状,大约就是这样:

在这里插入图片描述
图 8

严格地说,是这样:

在这里插入图片描述
图 9

我们将要构造这么一个表达式树,我们从叶子节点往根节点开始构建。

先给出最终的代码,再分析构建过程:

public static void Main()
{
    var selectProperty = "age";
    var person = new Person 
    {
	    Name = "Kit Lau",
	    Age = 18
    };
    ParameterExpression parameter = Expression.Parameter(typeof(Person));
    MemberExpression accessor = Expression.PropertyOrField(parameter, selectProperty);
    LambdaExpression lambda = Expression.Lambda(accessor, false, parameter);
    Console.WriteLine(lambda.Compile().DynamicInvoke(person)); // 18
}
  1. 首先,构建第一个叶子节点,树的左边部分:
在这里插入图片描述
图 10
   ParameterExpression parameter = Expression.Parameter(typeof(Person));

我们如何知道要使用 Expression.Parameter() 这个方法?看一下图 11,根节点的左子树的 NodeTypeParameter

在这里插入图片描述
图 11

再通过这个关键词去网络上查询如何构建 NodeTypeParameterExpression 即可。

  1. 现在我们有了这个 Person, 我们就可以获取它的 Name 了,构建右下的叶子节点:
在这里插入图片描述
图 12

也就是这部分:

在这里插入图片描述
图 13
   MemberExpression accessor = Expression.PropertyOrField(parameter, selectProperty);

可能有朋友会问,为什么不根据步骤 1,NodeTypeMemberAccess,从而调用与 MemberAccess 相关的方法呢?当然可以,输入 Expression.MemberAccrss 时,IDE 自动提示了 Expression.MakeMemberAccess 这个方法,那么代码就是这样:

   MemberExpression accessor = Expression.MakeMemberAccess(parameter, typeof(Person).GetProperty(selectProperty));

这会导致一个问题:如果 selectProperty 的值没有区分大小写,依旧使用小写字母开头的 "age",那么 typeof(Person).GetProperty(selectProperty) 会取不到值,所以用户输入的 selectProperty 的值就只能是大写字母开头的 "Age" 了,这样对用户很不方便。

  1. 再构建根节点,即使用前面构建的表达式组装为总的 lambda 表达式:
   LambdaExpression lambda = Expression.Lambda(accessor, false, parameter);
  1. 最后编译一下就可以调用了,把 person 对象作为参数传递给它:
   lambda.Compile().DynamicInvoke(person)

打印一下结果,是 18,没有问题。

4. 表达式树的常见用途:简化值对象的比较

在领域驱动设计(DDD)中有一个值对象的概念,使用 EF Core 查询时,当值对象作为参数时,比较起来会很麻烦,可以动态构建表达式树来简化这个过程。本来想一起写了,但是我懒了,明天再看,再水一篇新的,所以下一篇文章再介绍👀。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值