C#3.0新增功能10 表达式树 05 解释表达式

2019-09-17 18:25:58 浏览数 (1)

表达式树中的每个节点将是派生自 Expression 的类的对象。

该设计使得访问表达式树中的所有节点成为相对直接的递归操作。 常规策略是从根节点开始并确定它是哪种节点。

如果节点类型具有子级,则以递归方式访问该子级。 在每个子节点中,重复在根节点处使用的步骤:确定类型,且如果该类型具有子级,则访问每个子级。

检查不具有子级的表达式

让我们首先访问一个非常简单的表达式树中的每个节点。 下面是创建常数表达式然后检查其属性的代码:

代码语言:javascript复制
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}");

将打印以下内容:

代码语言:javascript复制
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 24

检查一个简单的加法表达式

从本节简介处的加法示例开始。

代码语言:javascript复制
Expression<Func<int>> sum = () => 1   2;

没有使用 var 来声明此表达式树,因为此操作无法执行,这是由于赋值右侧是隐式类型而导致的。 不能使用隐式类型化变量声明来声明 lambda 表达式。 它会对编译器造成循环逻辑问题。 var 声明会告知编译器通过赋值运算符右侧的表达式的类型查明变量的类型。 Lambda 表达式没有编译时类型,但是可转换为任何匹配委托或表达式类型。 将 lambda 表达式分配给委托或表达式类型的变量时,可告知编译器尝试并将 lambda 表达式转换为与“分配对象”变量的签名匹配的表达式或委托。 编译器必须尝试使赋值右侧的内容与赋值左侧的类型匹配。 赋值两侧都无法告知编译器查看赋值运算符另一侧的对象并查看我的类型是否匹配。

根节点是 LambdaExpression。 为了获得 => 运算符右侧的有用代码,需要找到 LambdaExpression 的子级之一。 我们将通过本部分中的所有表达式来实现此目的。 父节点确实有助于找到 LambdaExpression 的返回类型。

若要检查此表达式中的每个节点,将需要以递归方式访问大量节点。 下面是一个简单的首次实现:

代码语言:javascript复制
 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}");

此示例打印以下输出:

代码语言:javascript复制
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

以上代码示例中中包含大量重复。 为了将其其清理干净,并生成一个更加通用的表达式节点访问者。 这将要求编写递归算法。 任何节点都可能是具有子级的类型。 具有子级的任何节点都要求访问这些子级并确定该节点是什么。 下面是利用递归访问加法运算的已优化的版本:

代码语言:javascript复制
  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 会将消息打印到错误控制台。 如此,你便知道要添加新的表达式类型。)

在上面所示的加法表达式中运行此访问者时,将获得以下输出:

代码语言:javascript复制
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

检查具有许多级别的加法表达式

尝试更复杂的示例,但仍限制节点类型仅为加法:

代码语言:javascript复制
Expression<Func<int>> sum = () => 1   2   3   4;

在访问者算法上运行此表达式之前,请尝试思考可能的输出是什么。 请记住, 运算符是二元运算符:它必须具有两个子级,分别表示左右操作数。 有几种可行的方法来构造可能正确的树:

代码语言:javascript复制
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 的常量值。 编译器执行所有加法运算,并将表达式缩减为其最简单的形式。 只需在表达式中添加一个变量即可看到原始的树:

代码语言:javascript复制
Expression<Func<int, int>> sum = (a) => 1   a   3   4;

创建可得出此总和的访问者并运行该访问者,则会看到以下输出:

代码语言:javascript复制
 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 表达式(使用附加参数来阻止编译器计算常量)的一个示例:

代码语言:javascript复制
Expression<Func<int, int, int>> sum3 = (a, b) => (1   a)   (3   b);

下面是访问者的输出:

代码语言:javascript复制
 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

请注意,括号不是输出的一部分。 表达式树中不存在表示输入表达式中的括号的节点。 表达式树的结构包含传达优先级所需的所有信息。

从此示例扩展

此示例仅处理最基本的表达式树。 在本部分中看到的代码仅处理常量整数和二进制 运算符。 作为最后一个示例,让我们更新访问者以处理更加复杂的表达式。 让我们这样来改进它:

代码语言:javascript复制
Expression<Func<int, int>> factorial = (n) =>
    n == 0 
    ? 1 
    : Enumerable.Range(1, n).Aggregate((product, factor) => product * factor);

此代码表示数学 阶乘 函数的一个可能的实现。 编写此代码的方式强调了通过将 lambda 表达式分配到表达式来生成表达式树的两个限制。 首先,lambda 语句是不允许的。 这意味着无法使用循环、块、if / else 语句和 C# 中常用的其他控件结构。 我只能使用表达式。 其次,不能以递归方式调用同一表达式。 如果该表达式已是一个委托,则可以通过递归方式进行调用,但不能在其表达式树的形式中调用它。 在有关生成表达式树的部分中将介绍克服这些限制的技巧。

在此表达式中,将遇到所有这些类型的节点:

  1. Equal(二进制表达式)
  2. Multiply(二进制表达式)
  3. Conditional(? : 表达式)
  4. 方法调用表达式(调用 Range()Aggregate()

修改访问者算法的其中一个方法是持续执行它,并在每次到达 default 子句时编写节点类型。 经过几次迭代之后,便将看到每个可能的节点。 这样便万事俱备了。 结果类似于:

代码语言:javascript复制
 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 将处理这两个节点:

代码语言:javascript复制
 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 }

且表达式树的输出为:

代码语言:javascript复制
 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 标准中,且可以处理所有可能的节点类型。

0 人点赞