C#3.0新增功能10 表達式樹 07 翻譯(轉換)表達式

  • 2019 年 10 月 4 日
  • 筆記

  本篇將介紹如何訪問表達式樹中的每個節點,同時生成該表達式樹的已修改副本。 以下是在兩個重要方案中將使用的技巧。 第一種是了解表達式樹表示的演算法,以便可以將其轉換到另一個環境中。 第二種是何時更改已創建的演算法。 這可能是為了添加日誌記錄、攔截方法調用並跟蹤它們,或其他目的。

轉換即訪問

  生成的用於轉換表達式樹的程式碼是你已看到的用於訪問樹中所有節點的程式碼的擴展。 轉換表達式樹時,會訪問所有節點,並在訪問它們的同時生成新樹。 新樹可包含對原始節點的引用或已放置在樹中的新節點。

讓我們通過訪問表達式樹,並創建具有一些替換節點的新樹,來查看其工作原理。 在此示例中,我們將任何常數替換為其十倍大的常數。 我們通過將常數節點替換為執行乘法運算的新節點來進行此替換,而不必閱讀常數的值並將其替換為新的常數。

此處,在找到常數節點後,創建新乘法節點(其子節點是原始常數和常數 10):

private static Expression ReplaceNodes(Expression original)  {      if (original.NodeType == ExpressionType.Constant)      {          return Expression.Multiply(original, Expression.Constant(10));      }      else if (original.NodeType == ExpressionType.Add)      {          var binaryExpression = (BinaryExpression)original;          return Expression.Add(ReplaceNodes(binaryExpression.Left),                                ReplaceNodes(binaryExpression.Right));      }      return original;  }

通過替換原始節點,將形成一個包含修改的新樹。 可以通過編譯並執行替換的樹對此進行驗證。

var one = Expression.Constant(1, typeof(int));  var two = Expression.Constant(2, typeof(int));  var addition = Expression.Add(one, two);  var sum = ReplaceNodes(addition);  var executableFunc = Expression.Lambda(sum);    var func = (Func<int>)executableFunc.Compile();  var answer = func();  Console.WriteLine(answer);

生成新樹是兩者的結合:訪問現有樹中的節點,和創建新節點並將其插入樹中。

此示例演示了表達式樹不可變這一點的重要性。 請注意,上面創建的新樹混合了新創建的節點和現有樹中的節點。 這是安全的,因為現有樹中的節點無法進行修改。 這可以極大提高記憶體效率。 相同的節點可能會在整個樹或多個表達式樹中遍歷使用。 由於不能修改節點,因此可以在需要時隨時重用相同的節點。

遍歷並執行加法

  通過生成遍歷加法節點的樹並計算結果的第二個訪問者來對此進行驗證。 可以通過對目前見到的訪問者進行一些修改來執行此操作。 在此新版本中,訪問者將返回到目前為止加法運算的部分總和。 對於常數表達式,該總和即為常數表達式的值。 對於加法表達式,遍歷這些樹後,其結果為左操作數和右操作數的總和。

var one = Expression.Constant(1, typeof(int));  var two = Expression.Constant(2, typeof(int));  var three= Expression.Constant(3, typeof(int));  var four = Expression.Constant(4, typeof(int));  var addition = Expression.Add(one, two);  var add2 = Expression.Add(three, four);  var sum = Expression.Add(addition, add2);    // 聲明委託,這樣就可以從它本身遞歸地調用它  Func<Expression, int> aggregate = null;  // 聚合、返回常量或左、右操作數之和。  // 主要簡化:假設每個二進位表達式都是一個加法。  aggregate = (exp) => exp.NodeType == ExpressionType.Constant                     ? (int)((ConstantExpression)exp).Value                     : aggregate(((BinaryExpression)exp).Left) + aggregate(((BinaryExpression)exp).Right);  var theSum = aggregate(sum);  Console.WriteLine(theSum);

此處有相當多的程式碼,但這些概念是非常容易理解的。 此程式碼訪問首次深度搜索後的子級。 當它遇到常數節點時,訪問者將返回該常數的值。 訪問者訪問這兩個子級之後,這些子級將計算出為該子樹計算的總和。 加法節點現在可以計算其總和。 在訪問了表達式樹中的所有節點後,將計算出總和。 可以通過在調試器中運行示例並跟蹤執行來跟蹤執行。

讓我們通過遍歷樹,來更輕鬆地跟蹤如何分析節點以及如何計算總和。 下面是包含大量跟蹤資訊的聚合方法的更新版本:

private static int Aggregate(Expression exp)  {      if (exp.NodeType == ExpressionType.Constant)      {          var constantExp = (ConstantExpression)exp;          Console.Error.WriteLine($"Found Constant: {constantExp.Value}");          return (int)constantExp.Value;      }      else if (exp.NodeType == ExpressionType.Add)      {          var addExp = (BinaryExpression)exp;          Console.Error.WriteLine("Found Addition Expression");          Console.Error.WriteLine("Computing Left node");          var leftOperand = Aggregate(addExp.Left);          Console.Error.WriteLine($"Left is: {leftOperand}");          Console.Error.WriteLine("Computing Right node");          var rightOperand = Aggregate(addExp.Right);          Console.Error.WriteLine($"Right is: {rightOperand}");          var sum = leftOperand + rightOperand;          Console.Error.WriteLine($"Computed sum: {sum}");          return sum;      }      else throw new NotSupportedException("Haven't written this yet");  }

在同一表達式中運行該版本將生成以下輸出:

10  Found Addition Expression  Computing Left node  Found Addition Expression  Computing Left node  Found Constant: 1  Left is: 1  Computing Right node  Found Constant: 2  Right is: 2  Computed sum: 3  Left is: 3  Computing Right node  Found Addition Expression  Computing Left node  Found Constant: 3  Left is: 3  Computing Right node  Found Constant: 4  Right is: 4  Computed sum: 7  Right is: 7  Computed sum: 10  10

跟蹤輸出,並在上面的程式碼中跟隨。 應當能夠看出程式碼如何在遍歷樹的同時訪問程式碼和計算總和,並得出總和。

現在,讓我們來看看另一個運行,其表達式由 sum1 給出:

Expression<Func<int> sum1 = () => 1 + (2 + (3 + 4));

下面是通過檢查此表達式得到的輸出:

Found Addition Expression  Computing Left node  Found Constant: 1  Left is: 1  Computing Right node  Found Addition Expression  Computing Left node  Found Constant: 2  Left is: 2  Computing Right node  Found Addition Expression  Computing Left node  Found Constant: 3  Left is: 3  Computing Right node  Found Constant: 4  Right is: 4  Computed sum: 7  Right is: 7  Computed sum: 9  Right is: 9  Computed sum: 10  10

雖然最終結果是相同的,但樹遍歷完全不同。 節點的訪問順序不同,因為樹是以首先發生的不同運算構造的。

限制

存在一些不好翻譯成表達式樹的較新的 C# 語言元素。 表達式樹不能包含 await 表達式或 async lambda 表達式。 C# 6 發行中添加的許多功能不會完全按照表達式樹中所編寫的那樣顯示。 較新的功能可能會顯示在表達式樹中等效、早期的語法中。 這可能不像你想像的那樣有局限性。 實際上,這意味著在引入新語言功能時,解釋表達式樹的程式碼將仍可能照常運行。

即使具有這些限制,通過表達式樹,仍可創建依賴於解釋和修改表示為數據結構的程式碼的動態演算法。 它是一種功能強大的工具,作為 .NET 生態系統的一種功能,它可使豐富的庫(如實體框架)完成其所執行的操作。