反射是日常編碼中不可或缺的重要特性但是大規模應用又會造成效能問題,因此很多人都寫了提升反射速度的輔助類庫。在.net1.1時代主要應用Emit自己構造IL,這需要你精通IL而且還不能怕麻煩。而在.net2.0時代由於引入了泛型使我們可以利用泛型委派來減少一部分IL Emit過程(詳見這裡),但是泛型是編譯時間確定的而我們更多的應用是在運行時得到或設定對象的屬性,及動態執行方法。好在.net2.0又新增了一個輔助類DynamicMethod來協助我們進行Emit編程(詳見這裡)。如今.net3.5了微軟又給了我們更強大的武器Expression Tree(詳見這裡和這裡),利用它我們就可以避免使用複雜的IL而可以從更抽象更進階的角度來動態Emit我們需要的東西。是不是覺得.net發展越來越人性化呢易用話呢?就在我寫這篇文章的時候c#4.0的duck type估計已經出爐了,到時候Emit這技術也沒多大用了完全都是微軟替們我們搞定了(雖然編程的門檻降低了程式員越來越多了但我相信這些底層技術還是很有必要學習的,因為底層技術是一種解決問題的思路就像演算法)。
首先我要說明一下快速反射的原理:用委託來間接操作我們要Get,Set的屬性、欄位和要動態執行的方法。下面我以得到欄位的值來舉例說明。假設有一個User類定義如下:
- public class User
- {
- public int _age;
- public int Age
- {
- get{ return this._age; }
- set{ this._age = value;}
- }
- }
用傳統的反射方法是這樣的:
- User u = new User();
- u.Age = 26;
- PropertyInfo p = typeof(Employee).GetProperty("Age");
- int age = Convert.ToInt32(p.GetValue(u, null));
而使用快速反射的思路我們就要先拿到訪問屬性Age的MethodInfo然後構建一個Delegate,然後我們就能以調用函數的速度來動態取得Age屬性的值了。
- //如果你還在使用.net2.0需要定義Func
- public delegate R Func<T, R>(T t);
- User u = new User();
- u.Age = 26;
- PropertyInfo p = typeof(User).GetProperty("Age");
- //構建訪問Age屬性的委託,GetGetMethod是.net2.0新增的函數
- Func<User, int> getAge = (Func<User, int>)Delegate.CreateDelegate(typeof(Func<User, int>), p.GetGetMethod());
- int age = getAge(u);
泛型委派真是很好用,但是實際開發中能在編譯時間確定泛型參數機會不多,所以還要藉助DynamicMethod才能搞定(因為篇幅的關係請看codeproject的原文)。
Expression Tree可以方便的構造lambda運算式而lambda在.net中就是匿名委託的簡化形式。所以我們要實現快速存取屬性,欄位,方法只要構建出相應的lambda然後再用Expression Tree實現出來就行了。
1. Get Field/Property
泛型方式的lambda形如:Func<User, int> getAge = u => u.Age;
對應的Expression Tree為:
- var type = typeof(User);
- var p = type.GetProperty("Age");
- //lambda的參數u
- var param_u = Expression.Parameter(type, "u");
- //lambda的方法體 u.Age
- var pGetter = Expression.Property(param_u, p);
- //編譯lambda
- var getAge = Expression.Lambda<Func<User, int>>(pGetter, param_u).Compile();
非泛型方式的lambda形如:Func<object, object> getAge = obj => ((User)obj).Age;
對應的Expression Tree和上面的基本相同,只是要加入2個轉義語句:
- var type = typeof(User);
- var p = type.GetProperty("Age");
- //lambda的參數u
- var param_obj = Expression.Parameter(typeof(object), "obj");
- //類型轉換
- var convert_obj = Expression.Convert(param_obj, type);
- //lambda的方法體 ((User)obj).Age
- var pGetter = Expression.Property(convert_obj, p);
- //對傳回值進行類型轉換
- var returnObj = Expression.Convert(pGetter, typeof(object));
- //編譯lambda
- var getAge = Expression.Lambda<Func<object, object>>(returnObj, param_obj).Compile();
2.Set Field/Property
給欄位或屬性賦值的方法和上面的做法差別很大,因為在Expression中沒有進行賦值操作的相應方法。我認為沒有進行賦值的操作可能是和lambda有關,因為lambda是函數式編程中的概念。而在FP中函數都是無副作用的也就是說在函數中不能對參數或自由變數進行改變,這類似於.net中的string這種immutable類型。所以我使用方法調用的方式來修改值(如果你知道其他的解決方案一定要告訴我)。下面我以非泛型方式進行舉例:
- var u = new User();
- var p = typeof(User).GetProperty("Age");
- //對象執行個體
- var param_obj = Expression.Parameter(typeof(object), "obj");
- //值
- var param_val = Expression.Parameter(typeof(object), "val");
- //轉換參數為真實類型
- var body_obj = Expression.Convert(param_obj, type);
- var body_val = Expression.Convert(param_val, val.GetType());
- //調用給屬性賦值的方法
- body = Expression.Call(body_obj, p.GetSetMethod(), body_val);
- var setAge = Expression.Lambda<Action<object, object>>(body, param_obj, param_val).Compile();
- setAge(u, p);
上面這個例子是針對Property的但是對於Field來說是沒有GetSetMethod()這個方法的,所以我們要構造一個訪問欄位的方法:
- private static DynamicMethod GetField_SetMI(this FieldInfo fi)
- {
- var type = fi.DeclaringType;
- //DynamicMethod方便我們Emit IL
- DynamicMethod dm = new DynamicMethod(string.Empty, typeof(void), new Type[] { type, fi.FieldType }, type);
- ILGenerator il = dm.GetILGenerator();
- //在計算棧上傳入this
- il.Emit(OpCodes.Ldarg_0);
- //在計算棧上傳入要賦的值
- il.Emit(OpCodes.Ldarg_1);
- //設定欄位屬性
- il.Emit(OpCodes.Stfld, fi);
- //方法結束
- il.Emit(OpCodes.Ret);
- return dm;
- }
ok,得到了Set Field的MethodInfo我們只要稍加修改Set Property的代碼就可以了。
3. Call Method
動態執行方法調用和上面給欄位屬性賦值差不多隻是調用參數是不定的param object[] args,不過這裡要對參數做一點處理。首先要根據參數類型和數量找到相對應的MethodInfo,然後將參數進行轉義以便進行方法調用。這裡還要說明一點這種根據方法類型找相應重載方法的方式有可能不準確,所以要允許調用者手動提供準確的MethodInfo。下面以void傳回值的方法為例:
- //得到動態方法引動過程對應的委託,參數1:要調用的方法資訊,參數2:可變參數
- static Action<object, object[]> GetMethodDelegate(MethodInfo mi, object[] args)
- {
- var param_obj = Expression.Parameter(typeof(object), "obj");
- var param_args = Expression.Parameter(typeof(object[]), "args");
- //對可變參數進行轉義
- var body_args = new Expression[args == null ? 0 : args.Length];
- Expression body = null;
- var i = 0;
- foreach (var bodyarg in body_args)
- {
- //從param_args這個object[]中取值
- var index = Expression.Constant(i, typeof(int));
- var param_arg = Expression.ArrayIndex(param_args, index);
- //類型轉換
- body_args[i] = Expression.Convert(param_arg, args[i].GetType());
- i++;
- }
- //轉換成相應的對象執行個體類型
- var body_obj = Expression.Convert(param_obj, mi.DeclaringType);
- //方法調用
- body = Expression.Call(body_obj, mi, body_args);
- return Expression.Lambda<Action<object, object[]>>(body, param_obj, param_args).Compile();
- }
以上分別給出了針對Get/Set Field/Property和Call Method的Expression Tree構建代碼,但是在實際應用中這些還不夠。因為你還需要一個容器(如Dictionary)來儲存Compile後的lambda或者說delegate執行個體,因為動態編譯過程是很慢的所以這需要你在精加工一番。
好了從上面的執行個體代碼中不難看出運算式樹狀架構確實很方便使用,不過這裡還有一個問題我沒能解決就是如果處理帶ref 參數的函數。具體的說就是如何得到方法內修改後的值,如有下面方法
int Swap(ref int a, ref int b)
{
var tmp = a;
a = b;
b = tmp;
}
使用我上面給出的call method的方法確實可以調用,但是得不到被修改後的a和b的值。畢竟lambda不同於過程式風格,它可以這樣寫程式:
Func<int, Func<int, Func<int, int>>> func = a => b => c => a + b + c;
Console.WriteLine(func(4)(5)(6));
但是我現在還沒有想出如何利用closure和curring來解決ref參數修改值得問題,如果你想到了請告訴我。