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# 中常用的其他控制項結構。 我只能使用表達式。 其次,不能以遞歸方式調用同一表達式。 如果該表達式已是一個委託,則可以通過遞歸方式進行調用,但不能在其表達式樹的形式中調用它。 在有關生成表達式樹的部分中將介紹克服這些限制的技巧。
在此表達式中,將遇到所有這些類型的節點:
- 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 標準中,且可以處理所有可能的節點類型。