C#表示式樹淺析
一、前言
在我們日常開發中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”,我們在官方文件裡面可以看到很多介紹,具體資訊請檢視微軟官方文件庫:https://docs.microsoft.com/zh-cn/dotnet/api/system.linq.expressions?view=netcore-2.2;官方文件裡面的資訊量比較大,有幾十個物件的介紹:
這裡我不建議大家從頭到尾的看一遍,大致瀏覽就好了,因為資訊量太多了。首先我們新建一個控制檯程式,框架版本選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
至此,表示式樹已經完成了解析,上面的案例已經能滿足常用的需求了,若有其他要求我們可以繼續改造拓展解析方法。
四、總結
我們在學習技術的時候帶著一定的目的去學習往往效率更高,又不容易忘記,同時要善於思考,聯絡上下文情景。如果你覺得看完後對你有幫助可以給我點贊。
我寫完表示式解析後,發現另外一個網友對錶達式樹的分析很不錯:https://cloud.tencent.com/developer/article/1334993
程式碼已經放到GitHub:https://github.com/dianejason/ExpressionTreeDemo