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 標準中,且可以處理所有可能的節點類型。