好久沒更新這個系列了,最近看.NET CORE源碼的時候,發現他的依賴注入模塊的很多地方用了表達式拼接實現的。比如如下代碼
private Expression<Func<ServiceProviderEngineScope, object>> BuildExpression(IServiceCallSite callSite) { var context = new CallSiteExpressionBuilderContext { ScopeParameter = ScopeParameter }; var serviceExpression = VisitCallSite(callSite, context); if (context.RequiresResolvedServices) { return Expression.Lambda<Func<ServiceProviderEngineScope, object>>( Expression.Block( new [] { ResolvedServices }, ResolvedServicesVariableAssignment, Lock(serviceExpression, ResolvedServices)), ScopeParameter); } return Expression.Lambda<Func<ServiceProviderEngineScope, object>>(serviceExpression, ScopeParameter); }
所以今天我們先一起了解下表達式樹以及它的一種實用應用——表達式樹進行類的快速賦值。
提示:學習這一章,需要有一定拉姆達基礎,如果不太了解拉姆達,推薦閱讀《C#進階之路(四):拉姆達》。
一、初識表達式樹
表達式樹是將我們原來可以直接由代碼編寫的邏輯以表達式的方式存儲在樹狀的結構里,從而可以在運行時去解析這個樹,然后執行,實現動態的編輯和執行代碼。LINQ to SQL就是通過把表達式樹翻譯成SQL來實現的,所以了解表達樹有助於我們更好的理解 LINQ to SQL,同時如果你有興趣,可以用它創造出很多有意思的東西來。
根據Lambda表達式來創建表達式樹,這應該是最直接的創建表達式樹的方式了。
Expression<Func<int, int>> expr = x => x + 1; Console.WriteLine(expr.ToString()); // x=> (x + 1) // 下面的代碼編譯不通過 Expression<Func<int, int, int>> expr2 = (x, y) => { return x + y; }; Expression<Action<int>> expr3 = x => { };
這種方式只能創建最簡單的表達式樹,復雜點的編譯器就不認識了。
右邊是一個Lambda表達式,而左邊是一個表達式樹。為什么可以直接賦值呢?這個就要多虧我們的Expression<TDelegate>泛型類了。而Expression<TDelegate>是直接繼承自LambdaExpression的,我們來看一下Expression的構造函數:
internal Expression(Expression body, string name, bool tailCall,ReadOnlyCollection<ParameterExpression> parameters) : base(typeof(TDelegate), name, body, tailCall, parameters) { }
實際上這個構造函數什么也沒有做,只是把相關的參數傳給了父類,也就是LambdaExpression,由它把我們表達式的主體,名稱,以及參數保存着。
Expression<Func<int, int>> expr = x => x + 1; Console.WriteLine(expr.ToString()); // x=> (x + 1) var lambdaExpr = expr as LambdaExpression; Console.WriteLine(lambdaExpr.Body); // (x + 1) Console.WriteLine(lambdaExpr.ReturnType.ToString()); // System.Int32 foreach (var parameter in lambdaExpr.Parameters) { Console.WriteLine("Name:{0}, Type:{1}, ",parameter.Name,parameter.Type.ToString()); } //Name:x, Type:System.Int32
二、創建一個復雜的Lambda表達式樹
上面我們講到直接由Lambda表達式的方式來創建表達式樹,可惜只限於一種類型。下面我們就來演示一下如何創建一個無參無返回值的表達式樹。
// 下面的方法編譯不能過 /* Expression<Action> lambdaExpression2 = () => { for (int i = 1; i <= 10; i++) { Console.WriteLine("Hello"); } }; */ // 創建 loop表達式體來包含我們想要執行的代碼 LoopExpression loop = Expression.Loop( Expression.Call( null, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }), Expression.Constant("Hello")) ); // 創建一個代碼塊表達式包含我們上面創建的loop表達式 BlockExpression block = Expression.Block(loop); // 將我們上面的代碼塊表達式 Expression<Action> lambdaExpression = Expression.Lambda<Action>(block); lambdaExpression.Compile().Invoke();
上面我們通過手動編碼的方式創建了一個無參的Action,執行了一組循環。代碼很簡單,重要的是我們要熟悉這些各種類型的表達式以及他們的使用方式。上面我們引入了以下類型的表達式:
看起來神密的表達式樹也不過如此嘛?如果大家去執行上面的代碼,就會陷入死循環,我沒有為loop加入break的條件。為了方便大家理解,我是真的一步一步來啊,現在我們就來終止這個循環。就像上面那一段不能編譯通過的代碼實現的功能一樣,我們要輸出10個”Hello”。
上面我們先寫了一個LoopExpression,然后把它傳給了BlockExpresson,從而形成的的一塊代碼或者我們也可以說一個方法體。但是如果我們有多個執行塊,而且這多個執行塊里面需要處理同一個參數,我們就得在block里面聲明這些參數了。
ParameterExpression number=Expression.Parameter(typeof(int),"number"); BlockExpression myBlock = Expression.Block( new[] { number }, Expression.Assign(number, Expression.Constant(2)), Expression.AddAssign(number, Expression.Constant(6)), Expression.DivideAssign(number, Expression.Constant(2))); Expression<Func<int>> myAction = Expression.Lambda<Func<int>>(myBlock); Console.WriteLine(myAction.Compile()()); // 4
我們聲明了一個int的變量並賦值為2,然后加上6最后除以2。如果我們要用變量,就必須在block的你外面聲明它,並且在block里面把它引入進來。否則在該表達式樹時會出現,變量不在作用域里的錯。
下面我們繼續我們未完成的工作,為循環加入退出條件。為了讓大家快速的理解loop的退出機制,我們先來看一段偽代碼:
LabelTarget labelBreak = Expression.Label(); Expression.Loop( "如果 條件 成功" "執行成功的代碼" "否則" Expression.Break(labelBreak) //跳出循環 , labelBreak);
我們需要借助於LabelTarget 以及Expression.Break來達到退出循環的目地。下面我們來看一下真實的代碼:
LabelTarget labelBreak = Expression.Label(); ParameterExpression loopIndex = Expression.Parameter(typeof(int), "index"); BlockExpression block = Expression.Block( new[] { loopIndex }, // 初始化loopIndex =1 Expression.Assign(loopIndex, Expression.Constant(1)), Expression.Loop( Expression.IfThenElse( // if 的判斷邏輯 Expression.LessThanOrEqual(loopIndex, Expression.Constant(10)), // 判斷邏輯通過的代碼 Expression.Block( Expression.Call( null, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }), Expression.Constant("Hello")), Expression.PostIncrementAssign(loopIndex)), // 判斷不通過的代碼 Expression.Break(labelBreak) ),labelBreak)); // 將我們上面的代碼塊表達式 Expression<Action> lambdaExpression = Expression.Lambda<Action>(block); lambdaExpression.Compile().Invoke();
好吧,我們又學了幾個新的類型的表達式,來總結一下:
到這里,我想大家應該對表達式樹的構建有了一個清楚的認識。至於為什么不允許我們直接基於復雜的Lambda表達式來創建表達式樹呢?
這里的Lambda表達式實際上是一個Expression Body。
這個Expression Body實際上就是我們上面講到的Expression中的一種。
也就是說編譯器需要時間去分析你到底是哪一種?
最簡單的x=> x+1之類的也就是Func<TValue,TKey> 是很容易分析的。
實際這里面允許的Expression Body只有BinaryExpression。
最后,我們來完整的看一下.NET都為我們提供了哪些類型的表達式(下面這些類都是繼承自Expression)。

TypeBinaryExpression TypeBinaryExpression typeBinaryExpression = Expression.TypeIs( Expression.Constant("spruce"), typeof(int)); Console.WriteLine(typeBinaryExpression.ToString()); // ("spruce" Is Int32) IndexExpression ParameterExpression arrayExpr = Expression.Parameter(typeof(int[]), "Array"); ParameterExpression indexExpr = Expression.Parameter(typeof(int), "Index"); ParameterExpression valueExpr = Expression.Parameter(typeof(int), "Value"); Expression arrayAccessExpr = Expression.ArrayAccess( arrayExpr, indexExpr ); Expression<Func<int[], int, int, int>> lambdaExpr = Expression.Lambda<Func<int[], int, int, int>>( Expression.Assign(arrayAccessExpr, Expression.Add(arrayAccessExpr, valueExpr)), arrayExpr, indexExpr, valueExpr ); Console.WriteLine(arrayAccessExpr.ToString()); // Array[Index] Console.WriteLine(lambdaExpr.ToString()); // (Array, Index, Value) => (Array[Index] = (Array[Index] + Value)) Console.WriteLine(lambdaExpr.Compile().Invoke(new int[] { 10, 20, 30 }, 0, 5)); // 15 NewExpression NewExpression newDictionaryExpression =Expression.New(typeof(Dictionary<int, string>)); Console.WriteLine(newDictionaryExpression.ToString()); // new Dictionary`2() InvocationExpression Expression<Func<int, int, bool>> largeSumTest = (num1, num2) => (num1 + num2) > 1000; InvocationExpression invocationExpression= Expression.Invoke( largeSumTest, Expression.Constant(539), Expression.Constant(281)); Console.WriteLine(invocationExpression.ToString()); // Invoke((num1, num2) => ((num1 + num2) > 1000),539,281)
三、類的賦值
在代碼中經常會遇到需要把對象復制一遍,或者把屬性名相同的值復制一遍。
public class Student { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } } public class StudentSecond { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } }
反射
反射應該是很多人用過的方法,就是封裝一個類,反射獲取屬性和設置屬性的值。
private static TOut TransReflection<TIn, TOut>(TIn tIn) { TOut tOut = Activator.CreateInstance<TOut>(); var tInType = tIn.GetType(); foreach (var itemOut in tOut.GetType().GetProperties()) { var itemIn = tInType.GetProperty(itemOut.Name); ; if (itemIn != null) { itemOut.SetValue(tOut, itemIn.GetValue(tIn)); } } return tOut; }
調用:StudentSecond ss= TransReflection<Student, StudentSecond>(s);
調用一百萬次耗時:2464毫秒
序列化
序列化的方式有很多種,有二進制、xml、json等等,今天我們就用Newtonsoft的json進行測試。
調用:StudentSecond ss= JsonConvert.DeserializeObject<StudentSecond>(JsonConvert.SerializeObject(s));
調用一百萬次耗時:2984毫秒
從這可以看出序列化和反射效率差別不大。
四、表達式樹進行類的快速賦值
1、簡單實現
Expression<Func<Student, StudentSecond>> ss = (x) => new StudentSecond { Age = x.Age, Id = x.Id, Name = x.Name }; var f = ss.Compile(); StudentSecond studentSecond = f(s);
這樣的方式我們可以達到同樣的效果。
有人說這樣的寫法和最原始的復制沒有什么區別,代碼反而變多了呢,這個只是第一步。
2、分析代碼
我們用ILSpy反編譯下這段表達式代碼如下:
ParameterExpression parameterExpression; Expression<Func<Student, StudentSecond>> ss = Expression.Lambda<Func<Student, StudentSecond>>(Expression.MemberInit(Expression.New(typeof(StudentSecond)), new MemberBinding[] { Expression.Bind(methodof(StudentSecond.set_Age(int)), Expression.Property(parameterExpression, methodof(Student.get_Age()))), Expression.Bind(methodof(StudentSecond.set_Id(int)), Expression.Property(parameterExpression, methodof(Student.get_Id()))), Expression.Bind(methodof(StudentSecond.set_Name(string)), Expression.Property(parameterExpression, methodof(Student.get_Name()))) }), new ParameterExpression[] { parameterExpression }); Func<Student, StudentSecond> f = ss.Compile(); StudentSecond studentSecond = f(s);
那么也就是說我們只要用反射循環所有的屬性然后Expression.Bind所有的屬性。最后調用Compile()(s)就可以獲取正確的StudentSecond。
看到這有的人又要問了,如果用反射的話那豈不是效率很低,和直接用反射或者用序列化沒什么區別嗎?
當然這個可以解決的,就是我們的表達式樹可以緩存。只是第一次用的時候需要反射,以后再用就不需要反射了。
3、利用泛型的特性實現通用代碼
/// <summary> /// 表達式樹進行對象轉換 qxb /// </summary> /// <typeparam name="TIn"></typeparam> /// <typeparam name="TOut"></typeparam> public static class ExpTransHelper<TIn, TOut> { private static readonly Func<TIn, TOut> cache = GetFunc(); private static Func<TIn, TOut> GetFunc() { ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "p"); List<MemberBinding> memberBindingList = new List<MemberBinding>(); foreach (var item in typeof(TOut).GetProperties()) { if (!item.CanWrite) continue; if (typeof(TIn).GetProperty(item.Name) == null) { if (item.PropertyType == typeof(string)) { //將這個屬性綁定到"" MemberBinding memberString = Expression.Bind(item, Expression.Constant("")); memberBindingList.Add(memberString); } continue; } MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name)); MemberBinding memberBinding = Expression.Bind(item, property); memberBindingList.Add(memberBinding); } MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TOut)), memberBindingList.ToArray()); Expression<Func<TIn, TOut>> lambda = Expression.Lambda<Func<TIn, TOut>>(memberInitExpression, new ParameterExpression[] { parameterExpression }); return lambda.Compile(); } public static TOut Trans(TIn tIn) { return cache(tIn); } }
調用:StudentSecond ss= TransExpV2<Student, StudentSecond>.Trans(s);
調用一百萬次耗時:107毫秒
耗時小於使用automapper的338毫秒。