表达式树中的每个节点将是派生自 Expression
的类的对象。
该设计使得访问表达式树中的所有节点成为相对直接的递归操作。 常规策略是从根节点开始并确定它是哪种节点。
如果节点类型具有子级,则以递归方式访问该子级。 在每个子节点中,重复在根节点处使用的步骤:确定类型,且如果该类型具有子级,则访问每个子级。
var constant = Expression.Constant(24, typeof(int)); Console.WriteLine($"This is a/an {constant.NodeType} expression type"); Console.WriteLine($"The type of the constant value is {constant.Type}"); Console.WriteLine($"The value of the constant value is {constant.Value}");
This is an Constant expression type The type of the constant value is System.Int32 The value of the constant value is 24
Expression<Func<int>> sum = () => 1 + 2;
没有使用 var 来声明此表达式树,因为此操作无法执行,这是由于赋值右侧是隐式类型而导致的。
不能使用隐式类型化变量声明来声明 lambda 表达式。 它会对编译器造成循环逻辑问题。 var 声明会告知编译器通过赋值运算符右侧的表达式的类型查明变量的类型。 Lambda 表达式没有编译时类型,但是可转换为任何匹配委托或表达式类型。 将 lambda 表达式分配给委托或表达式类型的变量时,可告知编译器尝试并将 lambda 表达式转换为与“分配对象”变量的签名匹配的表达式或委托。 编译器必须尝试使赋值右侧的内容与赋值左侧的类型匹配。 赋值两侧都无法告知编译器查看赋值运算符另一侧的对象并查看我的类型是否匹配。
根节点是 LambdaExpression
。 为了获得 =>
运算符右侧的有用代码,需要找到 LambdaExpression
的子级之一。 我们将通过本部分中的所有表达式来实现此目的。 父节点确实有助于找到 LambdaExpression
的返回类型。
若要检查此表达式中的每个节点,将需要以递归方式访问大量节点。 下面是一个简单的首次实现:
1 Expression<Func<int, int, int>> addition = (a, b) => a + b; 2 3 Console.WriteLine($"This expression is a {addition.NodeType} expression type"); 4 Console.WriteLine($"The name of the lambda is {((addition.Name == null) ? "<null>" : addition.Name)}"); 5 Console.WriteLine($"The return type is {addition.ReturnType.ToString()}"); 6 Console.WriteLine($"The expression has {addition.Parameters.Count} arguments. They are:"); 7 foreach(var argumentExpression in addition.Parameters) 8 { 9 Console.WriteLine($"\tParameter Type: {argumentExpression.Type.ToString()}, Name: {argumentExpression.Name}"); 10 } 11 12 var additionBody = (BinaryExpression)addition.Body; 13 Console.WriteLine($"The body is a {additionBody.NodeType} expression"); 14 Console.WriteLine($"The left side is a {additionBody.Left.NodeType} expression"); 15 var left = (ParameterExpression)additionBody.Left; 16 Console.WriteLine($"\tParameter Type: {left.Type.ToString()}, Name: {left.Name}"); 17 Console.WriteLine($"The right side is a {additionBody.Right.NodeType} expression"); 18 var right= (ParameterExpression)additionBody.Right; 19 Console.WriteLine($"\tParameter Type: {right.Type.ToString()}, Name: {right.Name}");
此示例打印以下输出:
This expression is a Lambda expression type The name of the lambda is <null> The return type is System.Int32 The expression has 2 arguments. They are: Parameter Type: System.Int32, Name: a Parameter Type: System.Int32, Name: b The body is a Add expression The left side is a Parameter expression Parameter Type: System.Int32, Name: a The right side is a Parameter expression Parameter Type: System.Int32, Name: b
以上代码示例中中包含大量重复。 为了将其其清理干净,并生成一个更加通用的表达式节点访问者。 这将要求编写递归算法。 任何节点都可能是具有子级的类型。 具有子级的任何节点都要求访问这些子级并确定该节点是什么。 下面是利用递归访问加法运算的已优化的版本:
1 // Visitor 基类: 2 public abstract class Visitor 3 { 4 private readonly Expression node; 5 6 protected Visitor(Expression node) 7 { 8 this.node = node; 9 } 10 11 public abstract void Visit(string prefix); 12 13 public ExpressionType NodeType => this.node.NodeType; 14 public static Visitor CreateFromExpression(Expression node) 15 { 16 switch(node.NodeType) 17 { 18 case ExpressionType.Constant: 19 return new ConstantVisitor((ConstantExpression)node); 20 case ExpressionType.Lambda: 21 return new LambdaVisitor((LambdaExpression)node); 22 case ExpressionType.Parameter: 23 return new ParameterVisitor((ParameterExpression)node); 24 case ExpressionType.Add: 25 return new BinaryVisitor((BinaryExpression)node); 26 default: 27 Console.Error.WriteLine($"Node not processed yet: {node.NodeType}"); 28 return default(Visitor); 29 } 30 } 31 } 32 33 // Lambda Visitor 34 public class LambdaVisitor : Visitor 35 { 36 private readonly LambdaExpression node; 37 public LambdaVisitor(LambdaExpression node) : base(node) 38 { 39 this.node = node; 40 } 41 42 public override void Visit(string prefix) 43 { 44 Console.WriteLine($"{prefix}This expression is a {NodeType} expression type"); 45 Console.WriteLine($"{prefix}The name of the lambda is {((node.Name == null) ? "<null>" : node.Name)}"); 46 Console.WriteLine($"{prefix}The return type is {node.ReturnType.ToString()}"); 47 Console.WriteLine($"{prefix}The expression has {node.Parameters.Count} argument(s). They are:"); 48 // Visit each parameter: 49 foreach (var argumentExpression in node.Parameters) 50 { 51 var argumentVisitor = Visitor.CreateFromExpression(argumentExpression); 52 argumentVisitor.Visit(prefix + "\t"); 53 } 54 Console.WriteLine($"{prefix}The expression body is:"); 55 // Visit the body: 56 var bodyVisitor = Visitor.CreateFromExpression(node.Body); 57 bodyVisitor.Visit(prefix + "\t"); 58 } 59 } 60 61 // 二元运算 Visitor: 62 public class BinaryVisitor : Visitor 63 { 64 private readonly BinaryExpression node; 65 public BinaryVisitor(BinaryExpression node) : base(node) 66 { 67 this.node = node; 68 } 69 70 public override void Visit(string prefix) 71 { 72 Console.WriteLine($"{prefix}This binary expression is a {NodeType} expression"); 73 var left = Visitor.CreateFromExpression(node.Left); 74 Console.WriteLine($"{prefix}The Left argument is:"); 75 left.Visit(prefix + "\t"); 76 var right = Visitor.CreateFromExpression(node.Right); 77 Console.WriteLine($"{prefix}The Right argument is:"); 78 right.Visit(prefix + "\t"); 79 } 80 } 81 82 // 参数 visitor: 83 public class ParameterVisitor : Visitor 84 { 85 private readonly ParameterExpression node; 86 public ParameterVisitor(ParameterExpression node) : base(node) 87 { 88 this.node = node; 89 } 90 91 public override void Visit(string prefix) 92 { 93 Console.WriteLine($"{prefix}This is an {NodeType} expression type"); 94 Console.WriteLine($"{prefix}Type: {node.Type.ToString()}, Name: {node.Name}, ByRef: {node.IsByRef}"); 95 } 96 } 97 98 // 常量 visitor: 99 public class ConstantVisitor : Visitor 100 { 101 private readonly ConstantExpression node; 102 public ConstantVisitor(ConstantExpression node) : base(node) 103 { 104 this.node = node; 105 } 106 107 public override void Visit(string prefix) 108 { 109 Console.WriteLine($"{prefix}This is an {NodeType} expression type"); 110 Console.WriteLine($"{prefix}The type of the constant value is {node.Type}"); 111 Console.WriteLine($"{prefix}The value of the constant value is {node.Value}"); 112 } 113 }
此算法是可以访问任意 LambdaExpression
的算法的基础。 其中有大量缺口,即表明我创建的代码仅查找它可能遇到的表达式树节点组的一小部分。 但是,你仍可以从其结果中获益匪浅。 (遇到新的节点类型时,Visitor.CreateFromExpression
方法中的默认 case 会将消息打印到错误控制台。 如此,你便知道要添加新的表达式类型。)
在上面所示的加法表达式中运行此访问者时,将获得以下输出:
This expression is a/an Lambda expression type The name of the lambda is <null> The return type is System.Int32 The expression has 2 argument(s). They are: This is an Parameter expression type Type: System.Int32, Name: a, ByRef: False This is an Parameter expression type Type: System.Int32, Name: b, ByRef: False The expression body is: This binary expression is a Add expression The Left argument is: This is an Parameter expression type Type: System.Int32, Name: a, ByRef: False The Right argument is: This is an Parameter expression type Type: System.Int32, Name: b, ByRef: False
Expression<Func<int>> sum = () => 1 + 2 + 3 + 4;
在访问者算法上运行此表达式之前,请尝试思考可能的输出是什么。 请记住,+
运算符是二元运算符:它必须具有两个子级,分别表示左右操作数。 有几种可行的方法来构造可能正确的树:
Expression<Func<int>> sum1 = () => 1 + (2 + (3 + 4)); Expression<Func<int>> sum2 = () => ((1 + 2) + 3) + 4; Expression<Func<int>> sum3 = () => (1 + 2) + (3 + 4); Expression<Func<int>> sum4 = () => 1 + ((2 + 3) + 4); Expression<Func<int>> sum5 = () => (1 + (2 + 3)) + 4;
可以看到可能的答案分为两种,以便着重于最有可能正确的答案。 第一种表示右结合表达式。 第二种表示左结合表达式。 这两种格式的优点是,格式可以缩放为任意数量的加法表达式。
如果确实通过该访问者运行此表达式,则将看到此输出,其验证简单的加法表达式是否为左结合。
为了运行此示例并查看完整的表达式树,我不得不对源表达式树进行一次更改。 当表达式树包含所有常量时,所得到的树仅包含 10
的常量值。 编译器执行所有加法运算,并将表达式缩减为其最简单的形式。 只需在表达式中添加一个变量即可看到原始的树:
Expression<Func<int, int>> sum = (a) => 1 + a + 3 + 4;
创建可得出此总和的访问者并运行该访问者,则会看到以下输出:
1 This expression is a/an Lambda expression type 2 The name of the lambda is <null> 3 The return type is System.Int32 4 The expression has 1 argument(s). They are: 5 This is an Parameter expression type 6 Type: System.Int32, Name: a, ByRef: False 7 The expression body is: 8 This binary expression is a Add expression 9 The Left argument is: 10 This binary expression is a Add expression 11 The Left argument is: 12 This binary expression is a Add expression 13 The Left argument is: 14 This is an Constant expression type 15 The type of the constant value is System.Int32 16 The value of the constant value is 1 17 The Right argument is: 18 This is an Parameter expression type 19 Type: System.Int32, Name: a, ByRef: False 20 The Right argument is: 21 This is an Constant expression type 22 The type of the constant value is System.Int32 23 The value of the constant value is 3 24 The Right argument is: 25 This is an Constant expression type 26 The type of the constant value is System.Int32 27 The value of the constant value is 4
还可以通过访问者代码运行任何其他示例,并查看其表示的树。 下面是上述 sum3
表达式(使用附加参数来阻止编译器计算常量)的一个示例:
Expression<Func<int, int, int>> sum3 = (a, b) => (1 + a) + (3 + b);
下面是访问者的输出:
1 This expression is a/an Lambda expression type 2 The name of the lambda is <null> 3 The return type is System.Int32 4 The expression has 2 argument(s). They are: 5 This is an Parameter expression type 6 Type: System.Int32, Name: a, ByRef: False 7 This is an Parameter expression type 8 Type: System.Int32, Name: b, ByRef: False 9 The expression body is: 10 This binary expression is a Add expression 11 The Left argument is: 12 This binary expression is a Add expression 13 The Left argument is: 14 This is an Constant expression type 15 The type of the constant value is System.Int32 16 The value of the constant value is 1 17 The Right argument is: 18 This is an Parameter expression type 19 Type: System.Int32, Name: a, ByRef: False 20 The Right argument is: 21 This binary expression is a Add expression 22 The Left argument is: 23 This is an Constant expression type 24 The type of the constant value is System.Int32 25 The value of the constant value is 3 26 The Right argument is: 27 This is an Parameter expression type 28 Type: System.Int32, Name: b, ByRef: False
请注意,括号不是输出的一部分。 表达式树中不存在表示输入表达式中的括号的节点。 表达式树的结构包含传达优先级所需的所有信息。
+
运算符。 作为最后一个示例,让我们更新访问者以处理更加复杂的表达式。 让我们这样来改进它:Expression<Func<int, int>> factorial = (n) => n == 0
? 1
: Enumerable.Range(1, n).Aggregate((product, factor) => product * factor);
此代码表示数学 阶乘 函数的一个可能的实现。 编写此代码的方式强调了通过将 lambda 表达式分配到表达式来生成表达式树的两个限制。 首先,lambda 语句是不允许的。 这意味着无法使用循环、块、if / else 语句和 C# 中常用的其他控件结构。 我只能使用表达式。 其次,不能以递归方式调用同一表达式。 如果该表达式已是一个委托,则可以通过递归方式进行调用,但不能在其表达式树的形式中调用它。 在有关生成表达式树的部分中将介绍克服这些限制的技巧。
在此表达式中,将遇到所有这些类型的节点:
- Equal(二进制表达式)
- Multiply(二进制表达式)
- Conditional(? : 表达式)
- 方法调用表达式(调用
Range()
和Aggregate()
)
修改访问者算法的其中一个方法是持续执行它,并在每次到达 default
子句时编写节点类型。 经过几次迭代之后,便将看到每个可能的节点。 这样便万事俱备了。 结果类似于:
1 public static Visitor CreateFromExpression(Expression node) 2 { 3 switch(node.NodeType) 4 { 5 case ExpressionType.Constant: 6 return new ConstantVisitor((ConstantExpression)node); 7 case ExpressionType.Lambda: 8 return new LambdaVisitor((LambdaExpression)node); 9 case ExpressionType.Parameter: 10 return new ParameterVisitor((ParameterExpression)node); 11 case ExpressionType.Add: 12 case ExpressionType.Equal: 13 case ExpressionType.Multiply: 14 return new BinaryVisitor((BinaryExpression)node); 15 case ExpressionType.Conditional: 16 return new ConditionalVisitor((ConditionalExpression)node); 17 case ExpressionType.Call: 18 return new MethodCallVisitor((MethodCallExpression)node); 19 default: 20 Console.Error.WriteLine($"Node not processed yet: {node.NodeType}"); 21 return default(Visitor); 22 } 23 }
ConditionalVisitor 和 MethodCallVisitor 将处理这两个节点:
1 public class ConditionalVisitor : Visitor 2 { 3 private readonly ConditionalExpression node; 4 public ConditionalVisitor(ConditionalExpression node) : base(node) 5 { 6 this.node = node; 7 } 8 9 public override void Visit(string prefix) 10 { 11 Console.WriteLine($"{prefix}This expression is a {NodeType} expression"); 12 var testVisitor = Visitor.CreateFromExpression(node.Test); 13 Console.WriteLine($"{prefix}The Test for this expression is:"); 14 testVisitor.Visit(prefix + "\t"); 15 var trueVisitor = Visitor.CreateFromExpression(node.IfTrue); 16 Console.WriteLine($"{prefix}The True clause for this expression is:"); 17 trueVisitor.Visit(prefix + "\t"); 18 var falseVisitor = Visitor.CreateFromExpression(node.IfFalse); 19 Console.WriteLine($"{prefix}The False clause for this expression is:"); 20 falseVisitor.Visit(prefix + "\t"); 21 } 22 } 23 24 public class MethodCallVisitor : Visitor 25 { 26 private readonly MethodCallExpression node; 27 public MethodCallVisitor(MethodCallExpression node) : base(node) 28 { 29 this.node = node; 30 } 31 32 public override void Visit(string prefix) 33 { 34 Console.WriteLine($"{prefix}This expression is a {NodeType} expression"); 35 if (node.Object == null) 36 Console.WriteLine($"{prefix}This is a static method call"); 37 else 38 { 39 Console.WriteLine($"{prefix}The receiver (this) is:"); 40 var receiverVisitor = Visitor.CreateFromExpression(node.Object); 41 receiverVisitor.Visit(prefix + "\t"); 42 } 43 44 var methodInfo = node.Method; 45 Console.WriteLine($"{prefix}The method name is {methodInfo.DeclaringType}.{methodInfo.Name}"); 46 // There is more here, like generic arguments, and so on. 47 Console.WriteLine($"{prefix}The Arguments are:"); 48 foreach(var arg in node.Arguments) 49 { 50 var argVisitor = Visitor.CreateFromExpression(arg); 51 argVisitor.Visit(prefix + "\t"); 52 } 53 } 54 }
且表达式树的输出为:
1 This expression is a/an Lambda expression type 2 The name of the lambda is <null> 3 The return type is System.Int32 4 The expression has 1 argument(s). They are: 5 This is an Parameter expression type 6 Type: System.Int32, Name: n, ByRef: False 7 The expression body is: 8 This expression is a Conditional expression 9 The Test for this expression is: 10 This binary expression is a Equal expression 11 The Left argument is: 12 This is an Parameter expression type 13 Type: System.Int32, Name: n, ByRef: False 14 The Right argument is: 15 This is an Constant expression type 16 The type of the constant value is System.Int32 17 The value of the constant value is 0 18 The True clause for this expression is: 19 This is an Constant expression type 20 The type of the constant value is System.Int32 21 The value of the constant value is 1 22 The False clause for this expression is: 23 This expression is a Call expression 24 This is a static method call 25 The method name is System.Linq.Enumerable.Aggregate 26 The Arguments are: 27 This expression is a Call expression 28 This is a static method call 29 The method name is System.Linq.Enumerable.Range 30 The Arguments are: 31 This is an Constant expression type 32 The type of the constant value is System.Int32 33 The value of the constant value is 1 34 This is an Parameter expression type 35 Type: System.Int32, Name: n, ByRef: False 36 This expression is a Lambda expression type 37 The name of the lambda is <null> 38 The return type is System.Int32 39 The expression has 2 arguments. They are: 40 This is an Parameter expression type 41 Type: System.Int32, Name: product, ByRef: False 42 This is an Parameter expression type 43 Type: System.Int32, Name: factor, ByRef: False 44 The expression body is: 45 This binary expression is a Multiply expression 46 The Left argument is: 47 This is an Parameter expression type 48 Type: System.Int32, Name: product, ByRef: False 49 The Right argument is: 50 This is an Parameter expression type 51 Type: System.Int32, Name: factor, ByRef: False
本部分中的示例演示访问和检查表达式树中的节点的核心技术。 我略过了很多可能需要的操作,以便专注于访问表达式树中的节点这一核心任务。
首先,访问者只处理整数常量。 常量值可以是任何其他数值类型,且 C# 语言支持这些类型之间的转换和提升。 此代码的更可靠版本可反映所有这些功能。
即使最后一个示例也只可识别可能的节点类型的一部分。 你仍可以向其添加许多将导致其失败的表达式。 完整的实现包含在名为 ExpressionVisitor 的 .NET 标准中,且可以处理所有可能的节点类型。
个人作品
1、BIMFace.Community.SDK.NET
开源地址:https://gitee.com/NAlps/BIMFace.SDK·
·
2、ZCN.NET.Common
开源地址:https://gitee.com/NAlps/zcn.net.common
技术栈
1、Visual Studio、.C#/.NET、.NET Core、MVC、Web API、RESTful API、gRPC、SignalR、Python
2、jQuery、Vue.js、Bootstrap
3、数据库:SQLServer、MySQL、PostgreSQL、Oracle、SQLite、Redis、MongoDB、ElasticSearch、TiDB、达梦DM、人大金仓、 神通、南大通用 GBase、华为 GaussDB 、腾讯 TDSQL 、阿里 PolarDB、蚂蚁金服 OceanBase、东软 OpenBASE、浪潮云溪数据库 ZNBase
4、ORM:Dapper、Entity Framework、FreeSql、SqlSugar、分库分表、读写分离
5、架构:领域驱动设计 DDD、ABP
6、环境:跨平台、Windows、Linux(CentOS、麒麟、统信UOS、深度Linux)、maxOS、IIS、Nginx、Apach
7、移动App:Android、IOS、HarmonyOS、微信、小程序、快应用、Xamarin、uni-app、MUI、Flutter、Framework7、Cordova、Ionic、React Native、Taro、NutUI、Smobiler
云原生、微服务、Docker、CI/CD、DevOps、K8S;
Dapr、RabbitMQ、Kafka、分布式、大数据、高并发、负载均衡、中间件、RPC、ELK;
.NET + Docker + jenkins + Github + Harbor + K8S;·
·
作者:张传宁 微软MCP、系统架构设计师、系统集成项目管理工程师、科技部创新工程师。
专注于微软.NET技术(.NET Core、Web、MVC、WinForm、WPF)、通用权限管理系统、工作流引擎、自动化项目(代码)生成器、SOA 、DDD、 云原生(Docker、微服务、DevOps、CI/CD);PDF、CAD、BIM 审图等研究与应用。
多次参与电子政务、图书教育、生产制造等企业级大型项目研发与管理工作。
熟悉中小企业软件开发过程:需求分析、架构设计、编码测试、实施部署、项目管理。通过技术与管理帮助中小企业快速化实现互联网技术全流程解决方案。
本文版权归作者·有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。·
·