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

  • 2019 年 10 月 4 日
  • 筆記

表达式树中的每个节点将是派生自 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# 中常用的其他控件结构。 我只能使用表达式。 其次,不能以递归方式调用同一表达式。 如果该表达式已是一个委托,则可以通过递归方式进行调用,但不能在其表达式树的形式中调用它。 在有关生成表达式树的部分中将介绍克服这些限制的技巧。

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

  1. Equal(二进制表达式)
  2. Multiply(二进制表达式)
  3. Conditional(? : 表达式)
  4. 方法调用表达式(调用 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 标准中,且可以处理所有可能的节点类型。