C#表達式樹淺析

  • 2019 年 10 月 3 日
  • 筆記

一、前言

     在我們日常開發中Lamba 表達式經常會使用,如List.Where(n=>Name=”abc”) 使用起來非常的方便,程式碼也很簡潔,總之一個字就是“爽”。在之前我們總是用硬編碼的方式去實現一些底層方法,比如我要查詢用戶“abc”是否存在,老的實現方式基本為:GetUser(string name) ,可能下次我需要按用戶登錄帳號來查詢,於是又得寫一個方法:GetUserByLogin(string loginCode),我們認真想一下,如果能實現類似於集合查詢那樣只要寫lambda 就能搞定,List.Where(n=>Name=”abc”),List.Where(n=>LoginCode==”小A”),有了這樣的想法,那我們也去了解一下lambda 表達式樹的背後吧。

二、初識

      表達式樹關鍵字“Expressions”,我們在官方文檔裡面可以看到很多介紹,具體資訊請查看微軟官方文檔庫;官方文檔裡面的資訊量比較大,有幾十個對象的介紹:

這裡我不建議大家從頭到尾的看一遍,大致瀏覽就好了,因為資訊量太多了。首先我們新建一個控制台程式,框架版本選FX4.0以上或者Core 都行,引入命名空間:

using System.Linq.Expressions; 接下來實現一個簡單的功能,解析表達式: n=>n.Name=”abc” 我們想要的結果是 Name=”abc”,有了這個目標我們就知道該幹嘛了。

定義函數:private static string GetLambdaStr<T>(Expression<Func<T, bool>> expression),該函數定義了一個表達式樹參數,Func<in T,out bool>是范型委託,該委託表示接收一個T類型參數,返回一個bool值。具體程式碼:

        private static string GetLambdaStr<T>(Expression<Func<T, bool>> expression)          {              //解析表達式              //return new Analysis().AnalysisExpression(expression);
return null; }

 接下來建立一個用戶對象:

        public class User          {              public int ID { get; set; }              public string Name { get; set; }              public int Age { get; set; }              public bool States { get; set; }          }

再建立好測試程式碼:

        //Expression<Func<T, bool>> lambda = n => n.Name == "abc";              Console.WriteLine("lambda :  n => n.Name == "abc" ");              var a = GetLambdaStr<User>(n => n.Name == "abc");              Console.WriteLine("result:" + a);              Console.Write(Environment.NewLine);              Console.ReadKey();

先不管那麼多,我們調試進去看看錶達式長啥樣:

 

這樣看比較清真,就是一個lambda表達式,我們展開看看對象明細:

 

看到這裡我們是不是能想起點什麼了,這其實就是一顆二叉樹,顯示的層次為根節點,左子節點,右子節點,依次循環。有了這個認知,我們立馬能想到可以使用遞歸來遍歷節點了。

     於是我來了解表達式對象“Expression”有哪些屬性和方法:

看到這裡有點困惑了,剛剛我們明明看到有Left、Right 屬性,但這裡卻沒有,感覺好坑呀。沒有左右節點我們根本不知道怎麼去遞歸查找子節點呢。於是又去看官方介紹文檔了,然後再仔細看了LambdaExpression 對象,這個是抽象類,有抽象必定有相關的實現或者提供對外屬性,仔細一找,剛好找到BinaryExpression對象,有Left、Right屬性同時繼承了Expression對象,也提供了LambdaExpression 屬性,這個就是我們要找的對象了,可以說是峰迴路轉了:

 順著這個思路,我又找到了屬性成員和屬性值對象MemberExpression、ConstantExpression,我們來實現關鍵程式碼

        private static string GetLambdaStr<T>(Expression<Func<T, bool>> expression)
        {
            //解析表達式
            var body = (BinaryExpression)expression.Body;
            var r = (ConstantExpression)body.Right;
            var l = (MemberExpression)body.Left;
            var value = r.Type.IsValueType ? r.Value : $"'{r.Value}'";
            return $"{ l.Member.Name} {Operand(body.NodeType)} {value} ";
            // return new Analysis().AnalysisExpression(expression);
        }

Operand 是操作類型轉換,程式碼如下:

        //操作符轉換          private string Operand(ExpressionType type)          {              string operand = string.Empty;              switch (type)              {                  case ExpressionType.AndAlso:                      operand = "AND";                      break;                  case ExpressionType.OrElse:                      operand = "OR";                      break;                  case ExpressionType.Equal:                      operand = "=";                      break;                  case ExpressionType.LessThan:                      operand = "<";                      break;                  case ExpressionType.LessThanOrEqual:                      operand = "<=";                      break;                  case ExpressionType.GreaterThan:                      operand = ">";                      break;                  case ExpressionType.GreaterThanOrEqual:                      operand = ">";                      break;              }              return operand;          }

View Code

有了上面的程式碼我們已經完成功能了,運行結果如下:

三、進階

     日常開發中我們遇到的查詢條件可能會更加複雜,於是我又寫了幾個複雜得表達式:

            //Expression<Func<T, bool>> lambda = n => n.states;              Console.WriteLine("analysis: n => n.states ");              var b = GetLambdaStr<User>(n => n.States);              Console.WriteLine("result:" + b);              Console.ReadKey();                //Expression<Func<T, bool>> lambda = n => n.Name == "abc" && n.Age > 30 || n.ID == 4;              Console.WriteLine("lambda: n => n.Name == "abc" && n.Age > 30 || n.ID == 4");              var c = GetLambdaStr<User>(n => n.Name == "abc" && (n.Age > 30 || n.ID == 4) && n.ID > 1
&& (n.ID > 19 || n.Name == "33")); Console.WriteLine("result:" + c); Console.Write(Environment.NewLine); Console.ReadKey();

 經過我的一番探索和調試,終於完成了解析:

建議手動去敲一遍程式碼,並調試,這中間我遇到了一些坑,比如使用了OR條件時需要增加括弧,這個括弧老是沒放對位置。

最後貼出全部程式碼:

1、控制台程式碼

            //Expression<Func<T, bool>> lambda = n => n.Name == "abc";              Console.WriteLine("lambda :  n => n.Name == "abc" ");              var a = GetLambdaStr<User>(n => n.Name == "abc");              Console.WriteLine("result:" + a);              Console.Write(Environment.NewLine);              Console.ReadKey();                //Expression<Func<T, bool>> lambda = n => n.states;              Console.WriteLine("analysis: n => n.states ");              var b = GetLambdaStr<User>(n => n.States);              Console.WriteLine("result:" + b);              Console.ReadKey();                //Expression<Func<T, bool>> lambda = n => n.Name == "abc" && n.Age > 30 || n.ID == 4;              Console.WriteLine("lambda: n => n.Name == "abc" && n.Age > 30 || n.ID == 4");              var c = GetLambdaStr<User>(n => n.Name == "abc" && (n.Age > 30 || n.ID == 4) && n.ID > 1 && (n.ID > 19 || n.Name == "33"));              Console.WriteLine("result:" + c);              Console.Write(Environment.NewLine);              Console.ReadKey();

View Code

2、解析函數

        private static string GetLambdaStr<T>(Expression<Func<T, bool>> expression)          {              //解析表達式              return new Analysis().AnalysisExpression(expression);          }

View Code

3、用戶對象程式碼上面已經有了就不重複發了

4、解析對象程式碼

    public class Analysis      {          private StringBuilder builder = new StringBuilder();          public string AnalysisExpression<TDelegate>(Expression<TDelegate> expression)          {              if (expression.Body is MemberExpression)              {                  var m = (MemberExpression)expression.Body;                  var value = Convert.ToInt32(!expression.Body.ToString().Contains("!"));                  builder.Append($"  ({m.Member.Name}={value}) ");                  return builder.ToString();              }              var body = (BinaryExpression)expression.Body;              if (body.NodeType == ExpressionType.AndAlso || body.NodeType == ExpressionType.OrElse)              {                  AnalysisExpressionChild((BinaryExpression)body.Left, body.NodeType);                  AnalysisExpressionChild((BinaryExpression)body.Right, body.NodeType);              }              else              {                  var r = (ConstantExpression)body.Right;                  var l = (MemberExpression)body.Left;                  var value = r.Type.IsValueType ? r.Value : $"'{r.Value}'";                  builder.Append($" { l.Member.Name} {Operand(body.NodeType)} {value} ");              }              return builder.ToString();          }            //解析表達式樹          private void AnalysisExpressionChild(BinaryExpression expression, ExpressionType pType, string brackets = "")          {              if (expression.NodeType != ExpressionType.AndAlso && expression.NodeType != ExpressionType.OrElse)              {                  var r = (ConstantExpression)expression.Right;                  var l = (MemberExpression)expression.Left;                  var value = r.Type.IsValueType ? r.Value : $"'{r.Value}'";                  builder.Append($" {Operand(pType)} {brackets} { l.Member.Name} {Operand(expression.NodeType)} {value} ");              }              else              {                  if (expression.NodeType == ExpressionType.OrElse)                  {                      brackets = "( ";                  }                  AnalysisExpressionChild((BinaryExpression)expression.Left, pType, brackets);                  AnalysisExpressionChild((BinaryExpression)expression.Right, expression.NodeType);                  if (expression.NodeType == ExpressionType.OrElse)                  {                      builder.Append(" )");                  }              }          }            //操作符轉換          private string Operand(ExpressionType type)          {              string operand = string.Empty;              switch (type)              {                  case ExpressionType.AndAlso:                      operand = "AND";                      break;                  case ExpressionType.OrElse:                      operand = "OR";                      break;                  case ExpressionType.Equal:                      operand = "=";                      break;                  case ExpressionType.LessThan:                      operand = "<";                      break;                  case ExpressionType.LessThanOrEqual:                      operand = "<=";                      break;                  case ExpressionType.GreaterThan:                      operand = ">";                      break;                  case ExpressionType.GreaterThanOrEqual:                      operand = ">";                      break;              }              return operand;          }        }

View Code

 至此,表達式樹已經完成了解析,上面的案例已經能滿足常用的需求了,若有其他要求我們可以繼續改造拓展解析方法。

四、總結

      我們在學習技術的時候帶著一定的目的去學習往往效率更高,又不容易忘記,同時要善於思考,聯繫上下文情景。如果你覺得看完後對你有幫助可以給我點贊。

 

相關程式碼已經放到GitHub 

參考文檔:

1、微軟官方文檔庫:https://docs.microsoft.com/zh-cn/dotnet/api/system.linq.expressions?view=netcore-2.2

2、騰訊云:https://cloud.tencent.com/developer/article/1334993

五、補充

     經過繼續研究和分析網友的解析方法,發現其實微軟對表達式解析已經提供的了一個專門類:ExpressionVisitor,該類實現了對各種表達式操作的解析,我們直接繼承它, 所以我又重寫了一個解析類:AnalyseExpressionHelper,需要修改案例中調用解析方法的程式碼:

 

        private static string GetLambdaStr<T>(Expression<Func<T, bool>> expression)          {              //解析表達式三              var heler = new AnalyseExpressionHelper();              heler.AnalyseExpression(expression);              return heler.Result;          }

 

操作符轉換函數已經修改為拓展方法:

 

using System.Linq.Expressions;      namespace ExpressionTreeDemo  {      public static class ExpressionExtend      {          //操作符轉換          public static string TransferOperand(this ExpressionType type)          {              string operand = string.Empty;              switch (type)              {                  case ExpressionType.AndAlso:                      operand = "AND";                      break;                  case ExpressionType.OrElse:                      operand = "OR";                      break;                  case ExpressionType.Equal:                      operand = "=";                      break;                  case ExpressionType.NotEqual:                      operand = "<>";                      break;                  case ExpressionType.LessThan:                      operand = "<";                      break;                  case ExpressionType.LessThanOrEqual:                      operand = "<=";                      break;                  case ExpressionType.GreaterThan:                      operand = ">";                      break;                  case ExpressionType.GreaterThanOrEqual:                      operand = ">=";                      break;              }              return operand;          }      }  }

ExpressionExtend

 

新的表達式解析方法:

using System;  using System.Text;  using System.Linq.Expressions;    namespace ExpressionTreeDemo  {      /// <summary>      /// 表達式解析輔助類      /// </summary>      public class AnalyseExpressionHelper : ExpressionVisitor      {          private StringBuilder express = new StringBuilder();          public string Result { get { return express.ToString(); } }            public void AnalyseExpression<T>(Expression<Func<T, bool>> expression)          {              Visit(expression.Body);          }            protected override Expression VisitBinary(BinaryExpression node)          {              if (node.NodeType == ExpressionType.OrElse)                  express.Append("(");              Visit(node.Left);              express.Append($" {node.NodeType.TransferOperand()} ");              Visit(node.Right);              if (node.NodeType == ExpressionType.OrElse)                  express.Append(")");              return node;          }            protected override Expression VisitConstant(ConstantExpression node)          {              if (node.Type.IsValueType && node.Type != typeof(DateTime))              {                  express.Append(node.Value);              }              else              {                  express.Append($"'{node.Value}'");              }              return node;          }            protected override Expression VisitMember(MemberExpression node)          {              express.Append(node.Member.Name);              return node;          }      }  }

AnalyseExpressionHelper