環境:
- 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語句)。
未完待續。。。