1. 程式人生 > >C#表示式樹淺析

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