ASP.NET Core C# 反射 & 表达式树 (第三篇)
前言
前一篇讲完了反射, 这一篇来讲一下和反射息息相关的表达式树.
首先搞清楚 Delegate, Action, Func, Anonymous Method, Lambda, Expression tree
看大神的文章: C#中的Lambda表达式和表达式树
简单说, Delegate 委托是上古年代的东西,
后来有了泛型, 就演化成 Action 和 Func
然后开始玩匿名函数就有了 Lambda 表达式
然后又出来了一个表达式树.
Lambda 其实就是 Delegate, Action, Func 一样的东西.
而表达式树, 则是一些方法调用的表示手法.
表达式树由很多表达式组成, 经过 compile 可以变成 Lambda, 然后拿去执行.
也可以 parse 这个表达式树, 翻译成其它的东西.
比如 EF Core,
var person = await DbContext.People.Where(p => p.Name == "Derrick").ToListAsync();
上面这一句它可以翻译成 SQL
SELECT * FROM People WHERE Name = 'Derrick';
这篇不会讲到如何做翻译, 只会讲到如何动态创建表达式树.
Dynamic Expression How It Work
模拟一下动态创建 Expression for call EF Core's SingleAsync 方法.
public class Person { public string Name { get; set; } = ""; } public class Program { public static void Main() { var people = new List<Person> { new Person { Name = "n1" }, new Person { Name = "n2" }, new Person { Name = "n3" }, new Person { Name = "n4" }, }; var person = people.AsQueryable().Single(p => p.Name == "n1"); // p => p.Name == "n1" var pExp = Expression.Parameter(typeof(Person), "p"); // p var pDotNameExp = Expression.Property(pExp, "Name"); // p.Name var valueExp = Expression.Constant("n1"); // "n1" var pDotNameEqualValueExp = Expression.Equal(pDotName, value); // p.Name == "n1" var lambda = (Expression<Func<Person, bool>>)Expression.Lambda(pDotNameEqualValueExp, new ParameterExpression[] { pExp }); // p => p.Name == "n1" var lambdaDelegate = lambda.Compile(); // 把表达式树 compile 成可执行的委托 var person2 = people.AsQueryable().Single(lambda); // queryable 情况下参数是表达式, 因为要翻译成 SQL var person3 = people.Single(lambdaDelegate); // LINQ 的情况下参数是委托, 因为它是直接执行的 } }
需要动态创建的表达式是这一句 p => p.Name == "n1"
从上面可以看到, 它是逐个逐个通过 Expression 创建和拼接出来的. 看注释一句一句理解.
你会发现它的语法有一种 left right 的感觉, 然后拼着拼着就有点像棵树了, 二叉树
通常, 最后会把 Expression 变成 lambda 作为方法的参数, 需不需要 compile 就看最终拿来干什么. 如果是 Queryable 就直接给表达式树它翻译, 如果是 LINQ 就 compile 成委托执行.
多一个例子
var people = new List<Person> { new Person { Age = 1 }, new Person { Age = 2 }, new Person { Age = 3 }, new Person { Age = 4 }, }; var person = people.AsQueryable().Where(p => p.Age >= 1 && p.Age <= 3); // p => p.Age >= 1 && p.Age <= 3 var pExp = Expression.Parameter(typeof(Person), "p"); // p var pDotAgeExp = Expression.Property(pExp, "Age"); // p.Age var value1Exp = Expression.Constant(1); // 1 var pDotAgeGreatThanOrEqualvalue1Exp = Expression.GreaterThanOrEqual(pDotAgeExp, value1Exp); // p.Age >= 1 var value3Exp = Expression.Constant(3); // 3 var pDotAgeLessThanOrEqualvalue3Exp = Expression.LessThanOrEqual(pDotAgeExp, value3Exp); // p.Age <= 1 var and = Expression.And(pDotAgeGreatThanOrEqualvalue1Exp, pDotAgeLessThanOrEqualvalue3Exp); // p.Age >= 1 && p.Age <= 3 var lambda = (Expression<Func<Person, bool>>)Expression.Lambda(and, pExp); // p => p.Age >= 1 && p.Age <= 3 var lambdaDelegate = lambda.Compile(); var result = people.Where(lambdaDelegate).ToList(); // [1,2,3]
不管表达式如何复杂, 它就是逐个逐个去拼接, 所以不要害怕. Expression 里面有非常非常都多方法, 几乎你能写出来的 code 都能找到对应的 Expression
执行 lambdaDelegate
上面例子中, 我们都是把 Compile() 后的 lambdaDelegate pass 给 Single 和 Where 方法执行.
这里给一个自己执行的例子.
在第一篇 Anonymous method / Delegate, 我给过三种调用 delegate 的方式.
lambdaDelegate 也是委托, 按理说应该完全适用
第一种方式是类型清楚的, 这个没有问题
// width => width > 100; var widthParameterExp = Expression.Parameter(typeof(int), "width"); var greaterThan100Exp = Expression.GreaterThan(widthParameterExp, Expression.Constant(100)); var lambdaDelegate = Expression.Lambda<Func<int, bool>>(greaterThan100Exp, widthParameterExp).Compile(); var isGreaterThan100 = lambdaDelegate(150); // True
第二种是类型不清楚的
var lambdaDelegate = Expression.Lambda(greaterThan100Exp, widthParameterExp).Compile(); var isGreaterThan100 = (bool)lambdaDelegate.Method.Invoke(lambdaDelegate.Target, new object[] { 150 })!;
结果报错了 : MethodInfo must be a runtime MethodInfo
第三种 DynamicInvoke 则没有问题
var isGreaterThan100 = (bool)lambdaDelegate.DynamicInvoke(new object[] { 150 })!;
注意: DynamicInvoke 是很慢的, 虽然是搞反射, 但如果能知道部分类型, 比如第一种方式, 那就尽量用第一种吧.
Expression.Call
表达式中也可以表达方法调用哦. 当然这个对翻译 SQL 的话可能会有点问题啦. 但是 for 执行的话就很好用哦.
public class Person { public int Age { get; set; } public bool IsDeleted(int value) { return true; } } public class Program { public static void Main() { var people = new List<Person> { new Person { Age = 1 }, new Person { Age = 2 }, new Person { Age = 3 }, new Person { Age = 4 }, }; // p => p.IsDeleted(5) var pExp = Expression.Parameter(typeof(Person), "p"); // p var method = typeof(Person).GetMethod("IsDeleted")!; // 通过反射获取 MethodInfo var callMethodExp = Expression.Call(pExp, method, new Expression[] { Expression.Constant(5) }); // p.IsDeleted(5) var lambda = (Expression<Func<Person, bool>>)Expression.Lambda(callMethodExp, pExp); // p => p.IsDeleted(5) var lambdaDelegate = lambda.Compile(); var result = people.Where(lambdaDelegate).ToList(); // [1,2,3,4] } }
总结
动态创建表达式树, 可以帮助我们更好的管理 EF.Core 的代码. OData 也是通过这个方式去写入 EF Core 的 OData -> EF Core -> SQL.
以前我不喜欢写反射, 表达式树, 但后来我发现, 只要把小方法写好, 一点一点拼接上来, 它只是代码多, 但是并不会乱. 所以不要怕.
随便提一嘴,表达式树一直没有更新,许多 C# 新语法都不支持,相关 Github Issue – Extend expression trees to cover more/all language constructs