c#:表達式樹概念及應用場景(Expression)

環境:

  • window 10
  • vs2019 16.5.1
  • .netcore 3.1
  • .Net Reflector 10
  • ILSpy版本6.0.0.5559-preview2

參考:

目的:
探索什麼是表達式樹?
表達式能用來做什麼?
表達式樹在Entity Framework中起到了什麼作用?

一、什麼是表達式樹

在c#中,我們可以定義一種樹狀的數據結構來描述c#中的代碼,這種樹狀的數據結構就是表達式樹,也稱之爲表達式(各種表達式之間是可以相互嵌套的)。比如說:(5-2)+(2+3)這個表達式,拆分成樹狀結構如下圖:
在這裏插入圖片描述
當然,c#代碼肯定比這個要複雜的多,比如:你的定義語句、循環、判斷、屬性訪問等等。。。
在c#中,微軟爲每中運算類型的代碼定義了不同的表達式類型,它們有共同的基類:Expression。

表達式樹基類 Expression(抽象類)

它表示所有出現在c#中的代碼的類型,主要包含兩個屬性:

  • NodeType:這個表達式的節點類型,這是一個枚舉,c#中定義了85個節點類型
    在這裏插入圖片描述
  • Type:這個表達式的靜態類型,也就是這個表達式的返回類型

二、常用的表達式類型有哪些?

我們直接看有哪些表達式類是繼承自Expression的:
在這裏插入圖片描述
當然還有沒有直接繼承Expression的,比如:Expression<T>(我們最常用的表達式樹,繼承自LambdaExpression),這裏我就挑選幾個表達式樹簡單描述一下:

  • BinaryExpression:二元運算表達式
      1). Right:二元運算符的右側表達式(Expression)
      2). Left:二元運算符的左側表達式(Expression)
  • ConstantExpression:常量表達式
      1). Value:常量值,Object
  • ConditionalExpression:條件表達式
      1). IfFalse:爲False時的表達式(Expression)
      2). IfTrue:爲True時的表達式(Expression)
      3). Test:判斷表達式
  • ParameterExpression:參數表達式
      1). IsByRef:參數是否傳引用
      2). Name:參數名稱
  • LambdaExpression:lambda表達式
      1).Body:內容表達式(Expression)
      2).Parameters:參數表達式集合(ReadOnlyCollection)
      3).ReturnType:返回的類型
      4).Compile():方法,編譯生成委託
  • Expression<T>:帶有泛型的表達式,一般這個T就是委託類型的,繼承自LabelExpression

三、怎麼創建表達式樹

3.1 純手工組裝

3.1.1 將簡單lambda表達式:(x,y)=>x+y用表達式樹的形式表示出來

static void Main(string[] args)
{
     //組裝表達式樹
     ParameterExpression para1 = Expression.Parameter(typeof(int), "x");
     ParameterExpression para2 = Expression.Parameter(typeof(int), "y");
     var paras = new ParameterExpression[] { para1, para2 };
     BinaryExpression body = Expression.Add(para1, para2);
     var expression = Expression.Lambda(body, paras);
     //編譯表達式樹
     var func = expression.Compile() as Func<int, int, int>;
     //調用執行
     var res = func(1, 2);
     Console.WriteLine($"表達式樹的執行結果:(1,2)=>{res}");
     Console.WriteLine("Hello World!");
     Console.ReadLine();
 }

運行如下:
在這裏插入圖片描述

3.1.2 將複雜的方法定義用表達式樹的形式表示出來

原方法定義:

public static string GetScoreDesc(string name, int score, int standard)
{
    string desc = "";
    if (score > standard)
    {
        desc = "已及格";
    }
    else if (score == standard)
    {
        desc = "剛及格";
    }
    else
    {
        desc = "未及格";
    }
    return name + desc;
}

用表達式樹表示如下:

private static void Test()
{
	//定義三個參數: string name,ing score,int standard
    ParameterExpression paraName = Expression.Parameter(typeof(string), "name");
    ParameterExpression paraScore = Expression.Parameter(typeof(int), "score");
    ParameterExpression paraStandard = Expression.Parameter(typeof(int), "standard");
    //定義一個局部變量:string desc;
    ParameterExpression defineDesc = Expression.Parameter(typeof(string), "desc");
    //給局部變量賦值:desc="";
    BinaryExpression assignDesc = Expression.Assign(defineDesc, Expression.Constant(""));
    //準備賦值語句:desc="已及格"
    BinaryExpression assignDescYijige = Expression.Assign(defineDesc, Expression.Constant("已及格"));
    //準備賦值語句:desc="剛及格"
    BinaryExpression assignDescGangjige = Expression.Assign(defineDesc, Expression.Constant("剛及格"));
    //準備賦值語句:desc="未及格"
    BinaryExpression assignDescWeijige = Expression.Assign(defineDesc, Expression.Constant("未及格"));
    //準備代碼:score>standard
    BinaryExpression greaterThan = Expression.MakeBinary(ExpressionType.GreaterThan, paraScore, paraStandard);
    //準備代碼:score==standard
    BinaryExpression equalThan = Expression.MakeBinary(ExpressionType.Equal, paraScore, paraStandard);
    //準備代碼:score<standard
    BinaryExpression lessThan = Expression.MakeBinary(ExpressionType.LessThan, paraScore, paraStandard);
    //組裝判斷邏輯塊:if(score>standard){desc="已及格"}else{if(score==standard){desc="剛及格"}else{desc="未及格"}}
    ConditionalExpression conditional = Expression.Condition(greaterThan, assignDescYijige, Expression.Condition(equalThan, assignDescGangjige, assignDescWeijige));
    //準備代碼:name+desc (注意:methodof運算符是用c#自定義的,代碼在後面)
    BinaryExpression addAssign = Expression.Add(paraName, defineDesc, (methodof<Func<string, string, string>>)(String.Concat));
    //定義標記:這個標記用於方法返回值
    LabelTarget labelTarget = Expression.Label(typeof(string));
    //定義返回代碼段:默認返回 desc
    LabelExpression labelExpression = Expression.Label(labelTarget, defineDesc);
    //定義return語句(這裏是goto語句):goto labelTarget 
    GotoExpression gotoExpression = Expression.Return(labelTarget, addAssign, typeof(string));
    //組裝這個方法的方法體(指定局部變量)
    BlockExpression block = Expression.Block(typeof(string), new ParameterExpression[] { defineDesc }, assignDesc, conditional, gotoExpression, labelExpression);
    //完成組裝這個方法(指定參數)
    LambdaExpression expression = Expression.Lambda<Func<string, int, int, string>>(block, new ParameterExpression[] { paraName, paraScore, paraStandard });

	//編譯測試...
    var delega = expression.Compile();
    var func = delega as Func<string, int, int, string>;
    var res = func("xiaoming", 5, 6);
    Console.WriteLine($"func(\"xiaoming\", 5, 6)=>{func("xiaoming", 5, 6)}");
    Console.WriteLine($"func(\"xiaoming\", 2, 6)=>{func("xiaoming", 2, 6)}");
    Console.WriteLine($"func(\"xiaoming\", 6, 6)=>{func("xiaoming", 6, 6)}");
    Console.WriteLine($"func(\"xiaoming\", 8, 6)=>{func("xiaoming", 8, 6)}");
	
	//嘗試打印出表達式樹的字符串形式    
    Console.WriteLine("整體輸出...");
    Console.WriteLine(expression.ToString());
    Console.WriteLine("--------------------------start");
    Console.WriteLine(assignDesc.ToString());
    Console.WriteLine(conditional);
    Console.WriteLine(gotoExpression);
    Console.WriteLine(labelExpression);
    Console.WriteLine("---------------------------end");
}

上面用到的methodof運算符定義如下:

public class methodof<T>
{
    private MethodInfo method;

    public methodof(T func)
    {
        Delegate del = (Delegate)(object)func;
        this.method = del.Method;
    }

    public static implicit operator methodof<T>(T methodof)
    {
        return new methodof<T>(methodof);
    }

    public static implicit operator MethodInfo(methodof<T> methodof)
    {
        return methodof.method;
    }
}

整個代碼輸出如下:
在這裏插入圖片描述
從上面可以看到表達式樹不僅可以組裝簡單的lambda表達式,還可以帶語句塊的複雜方法。

3.2 使用編譯器的語法糖

同樣是上面的lambda表達式`:(x,y)=>x+y,代碼如下:

static void Main(string[] args)
{
    //組裝表達式樹
    Expression<Func<int, int, int>> expression = (x, y) => x + y;
    //編譯並調用
    var res = expression.Compile()(1, 2);
    Console.WriteLine($"表達式樹的執行結果:(1,2)=>{res}");
    Console.WriteLine("Hello World!");
    Console.ReadLine();
}

毫無疑問,運行效果是一樣的:
在這裏插入圖片描述
我們用.Net Reflector反編譯看一下:
在這裏插入圖片描述
可以看到,這就是個語法糖。我們在entity framework中經常會用到這種語法糖,看下圖:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
從圖中可以看到,entity framework中的參數傳遞用的都是表達式樹(而不是我們熟悉的委託。。。),但一般我們穿進去的是lambda表達式,這裏也是語法糖:編譯器會在編譯的時候將你寫的委託組裝成表達式樹再傳進入:
在這裏插入圖片描述
注意:
這個語法糖有一個限制:就是你的lambda代碼不能有代碼塊(而這個特點正好被用在了entity framework上防止你任意的輸入無限量代碼)!
看下圖:
在這裏插入圖片描述

四、表達式樹能做什麼?

上面講到了,手動組裝表達式樹然後編譯調用,現實中肯定不能這麼用,如果是自己組裝表達式再自己調用的話,爲什麼不乾脆直接調用委託?
所以說,表達式樹是給框架的作者用的。試想一下:當你調用框架方法的時候,你會傳入一個簡單的lambda表達式(lambda表達式還是很方便的),然後框架拿到你傳入的表達式樹(編譯器編譯時就已經將你的lambda表達式改裝成了表達式樹)並進行分析結構,然後做一些東西(對於entity framework來說就是分析表達式樹 -> 生成適當sql語句->執行sql語句)。

未完待續。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章