.Net 的反射是個很好很強大的東西,不過它的效率卻實在是不給力。已經有很多人針對這個問題討論過了,包括各種各樣的 DynamicMethod 和各種各樣的效率測試,不過總的來說解決方案就是利用 Expression Tree、Delegate.CreateDelegate 或者 Emit 構造出反射操作對應的委托,從而實現加速反射的目的。
雖然本篇文章同樣是討論利用委托來加速反射調用函數,不過重點並不在於如何提升調用速度,而是如何更加智能的構造出反射的委托,並最終完成一個方便易用的委托創建器 DelegateBuilder。
它的設計目標是:
- 能夠對方法調用、構造函數調用,獲取或設置屬性和獲取或設置字段提供支持。
- 能夠構造出特定的委托類型,而不僅限於 Func<object, object[], object> 或者其它的 Func 和 Action,因為我個人很喜歡強類型的委托,同時類似 void MyDeleagte(params int[] args) 這樣的委托有時候也是很有必要的,如果需要支持 ref 和 out 參數,就必須使用自定義的委托類型了。
- 能夠支持泛型方法,因為利用反射選擇泛型方法是件很糾結的事(除非沒有同名方法),而且還需要再 MakeGenericMethod。
- 能夠支持類型的顯式轉換,在對某些 private 類的實例方法構造委托時,實例本身就必須使用 object 傳入才可以。
其中的 3、4 點,在前幾篇隨筆《C# 判斷類型間能否隱式或強制類型轉換》和《C# 泛型方法的類型推斷》中已經被解決了,並且整合到了 PowerBinder 中,這里只要解決 1、2 點就可以了,這篇隨筆就是來討論如何根據反射來構造出相應的委托。
就目前完成的效果,DelegateBuilder 可以使用起來還是非常方便的,下面給出一些示例:
class Program { public delegate void MyDelegate(params int[] args); public static void TestMethod(int value) { } public void TestMethod(uint value) { } public static void TestMethod<T>(params T[] arg) { } static void Main(string[] args) { Type type = typeof(Program); Action<int> m1 = type.CreateDelegate<Action<int>>("TestMethod"); m1(10); Program p = new Program(); Action<Program, uint> m2 = type.CreateDelegate<Action<Program, uint>>("TestMethod"); m2(p, 10); Action<object, uint> m3 = type.CreateDelegate<Action<object, uint>>("TestMethod"); m3(p, 10); Action<uint> m4 = type.CreateDelegate<Action<uint>>("TestMethod", p); m4(10); MyDelegate m5 = type.CreateDelegate<MyDelegate>("TestMethod"); m5(0, 1, 2); } }
可以說效果還是不錯的,這里的 CreateDelegate 的用法與 Delegate.CreateDelegate 完全相同,功能卻大大豐富,幾乎可以只依靠 delegate type、type 和 memberName 構造出任何需要的委托,省去了自己反射獲取類型成員的過程。
這里特別要強調一點:這個類用起來很簡單,但是簡單的背后是實現的復雜,所以各種沒有發現的 bug 和推斷錯誤是很正常的。
我再補充一點:雖然在這里我並不打算討論效率問題,但的確有不少朋友對效率問題有點糾結,我就來詳細解釋下這個問題。
第一個問題:為什么要用委托來代替反射。如果手頭有 Reflector 之類的反編譯軟件,可以看看 System.Reflection.RuntimeMethodInfo.Invoke 方法的實現,它首先需要檢查參數(檢查默認參數、類型轉換之類的),然后檢查各種 Flags,然后再調用 UnsafeInvokeInternal 完成真正的調用過程,顯然比直接調用方法要慢上不少。而如果利用 Expression Tree 之類的方法構造出了委托,它就相當於只多了一層方法調用,性能不會損失多少(據說如果 Emit 用得好還能更快),因此才需要利用委托來代替反射。
第二個問題:什么時候適合用委托來代替反射。現在假設有一家公園,它的門票是 1 元,它還有一種終身票,票價是 20 元。如果我只是想進去看看,很可能以后就不再去了,那么我直接花 1 元進去是最合適的。但如果我想天天去溜達溜達,那么花 20 元買個終身票一定更加合適。
相對應的,1 元的門票就是反射,20 元的終身票就是委托——如果某個方法我只是偶爾調用一下,那么直接用反射就好了,反正損失也不是很大;如果我需要經常調用,花點時間構造個委托出來則是更好的選擇,雖然構造委托這個過程比較慢,但它受用終身的。
第三個問題:怎么測試委托和反射的效率。測試效率的前提就是假設某個方法是需要被經常調用的,否則壓根沒必要使用委托。那么,基本的結構如下所示:
Stopwatch sw = new Stopwatch(); Type type = typeof(Program); sw.Start(); Action<int> action = type.CreateDelegate<Action<int>>("TestMethod"); for (int i = 0; i < 10000; i++) { action(i); } sw.Stop(); Console.WriteLine("DelegateBuilder:{0} ms", sw.ElapsedMilliseconds); sw.Start(); MethodInfo method = type.GetMethod("TestMethod"); for (int i = 0; i < 10000; i++) { method.Invoke(null, new object[] { i }); } sw.Stop(); Console.WriteLine("Reflection:{0} ms", sw.ElapsedMilliseconds);
這里將構造委托的過程和反射得到 MethodInfo 的過程都放在了循環的外面,是因為它們只需要獲取一次,就可以一直使用的(也就是所謂的“預處理”)。至於時候將它們放在 StopWatch 的 Start 和 Stop 之間,就看是否想將預處理所需的時間也計算在內了。
目前我能想到的問題就這三個了,如果還有什么其它相關問題,可以聯系我。
言歸正傳,下面就來分析如何為反射構造出相應的委托。為了簡便起見,我將使用 Expression Tree 來構造委托,這樣更加易讀,而且效率也並不會比 Emit 低多少。對於 Expression 不熟悉的朋友可以參考 Expression 類。
一、從 MethodInfo 創建方法的委托
首先從創建方法的委托說開來,因為方法的委托顯然是最常用、最基本的了。Delegate 類為我們提供了一個很好的參考,它的 CreateDelegate 方法有十個重載,這些重載之間的關系可以用下面的圖表示出來,他們的詳細解釋可見 MSDN:
圖1 Delegate.CreateDelegate
這些方法的確很給力,用起來也比較方便,盡管在我看來還不夠強大:)。為了易於上手,自己的方法委托創建方法的行為也應該類似於 Delegate.CreateDelegate 方法,因此接下來會先分析 CreateDelegate 方法的用法,然后再解釋如何自己創建委托。
1.1 創建開放的方法委托
CreateDelegate(Type, MethodInfo) 和 CreateDelegate(Type, MethodInfo, Boolean) 的功能是相同的,都是可以創建靜態方法的委托,或者是顯式提供實例方法的第一個隱藏參數(稱開放的實例方法,從 .Net Framework 2.0 以后支持)的委托。以下面的類為例:
class TestClass { public static void TestStaticMethod(string value) {} public void TestMethod(string value) {} }
要創建 TestStaticMethod 方法的委托,需要使用 Action<string> 委托類型,代碼為
Delegate.CreateDelegate(typeof(Action<string>), type.GetMethod("TestStaticMethod"))
得到的委托的效果與 TestStaticMethod(arg1) 相同。
要創建 TestMethod 方法的委托,則需要使用 Action<TestClass, string> 委托類型才可以,第一個參數表示要在其上調用方法的 TestClass 的實例:
Delegate.CreateDelegate(typeof(Action<TestClass, string>), type.GetMethod("TestMethod"))
得到的委托的效果與 arg1.TestMethod(arg2) 相同。
這個方法的用法很明確,自己實現起來也非常簡單:
首先對開放的泛型方法構造相應的封閉的泛型方法,做法與上一篇《C# 使用 Binder 類自定義反射》中的 2.2.2 處理泛型方法 一節使用的算法相同,這里就不再贅述了。
接下就可以直接利用 Expression.Call 創建一個方法調用的委托,並對每個參數添加一個強制類型轉換(Expression.Convert)即可。需要注意的是如果 MethodInfo 是實例方法,那么第一個參數要作為實例使用。最后用 Expression 構造出來的方法應該類似於:
// method 對應於靜態方法。 returnType MethodDelegate(PT0 p0, PT1 p1, ... , PTn pn) { return method((T0)p0, (T1)p1, ... , (Tn)pn); } // method 對應於實例方法。 returnType MethodDelegate(PT0 p0, PT1 p1, ... , PTn pn) { return ((T0)p0).method((T1)p1, ... , (Tn)pn); }
構造開放的方法委托的核心方法如下所示:
private static Delegate CreateOpenDelegate(Type type, MethodInfo invoke, ParameterInfo[] invokeParams, MethodInfo method, ParameterInfo[] methodParams) { // 要求參數數量匹配,其中實例方法的第一個參數用作傳遞實例對象。 int skipIdx = method.IsStatic ? 0 : 1; if (invokeParams.Length == methodParams.Length + skipIdx) { if (method.IsGenericMethodDefinition) { // 構造泛型方法的封閉方法,對於實例方法要跳過第一個參數。 Type[] paramTypes = GetParameterTypes(invokeParams, skipIdx, 0, 0); method = method.MakeGenericMethodFromParams(methodParams, paramTypes); if (method == null) { return null; } methodParams = method.GetParameters(); } // 方法的參數列表。 ParameterExpression[] paramList = GetParameters(invokeParams); // 構造調用參數列表。 Expression[] paramExps = GetParameterExpressions(paramList, skipIdx, methodParams, 0); if (paramExps != null) { // 調用方法的實例對象。 Expression instance = null; if (skipIdx == 1) { instance = ConvertType(paramList[0], method.DeclaringType); if (instance == null) { return null; } } Expression methodCall = Expression.Call(instance, method, paramExps); methodCall = GetReturn(methodCall, invoke.ReturnType); if (methodCall != null) { return Expression.Lambda(type, methodCall, paramList).Compile(); } } } return null; }
1.2 創建第一個參數封閉的方法委托
CreateDelegate(Type, Object, MethodInfo) 和 CreateDelegate(Type, Object, MethodInfo, Boolean) 是最靈活的創建委托的方法,可以創建靜態或實例方法的委托,可以提供或不提供第一個參數。先來給出所有用法的示例:
class TestClass { public static void TestStaticMethod(string value) {} public void TestMethod(string value) {} }
對於 TestStaticMethod (靜態方法)來說:
- 若 firstArgument 不為 null,則在每次調用委托時將其傳遞給方法的第一個參數,此時稱為通過第一個參數封閉,要求委托的簽名包括方法除第一個參數之外的所有參數,使用方法為
Delegate.CreateDelegate(typeof(Action), "str", type.GetMethod("TestStaticMethod"))
- 若 firstArgument 為 null,且委托和方法的簽名匹配(即所有參數類型都兼容),則此時稱為開放的靜態方法委托,使用方法為
Delegate.CreateDelegate(typeof(Action<string>), null, type.GetMethod("TestStaticMethod"))
- 若 firstArgument 為 null,且委托的簽名以方法的第二個參數開頭,其余參數類型都兼容,則此時稱為通過空引用封閉的委托,使用方法為
Delegate.CreateDelegate(typeof(Action), null, type.GetMethod("TestStaticMethod"))
對於 TestMethod (實例方法)來說:
- 若 firstArgument 不為 null,則 firstArgument 被傳遞給隱藏的實例參數(就是 this),這時成為封閉的實例方法,要求委托的簽名必須和方法的簽名匹配,使用方法為
Delegate.CreateDelegate(typeof(Action<string>), new TestClass(), type.GetMethod("TestMethod"))
- 若 firstArgument 為 null,且委托顯示包含方法的第一個隱藏參數(就是 this),則此時稱為開放的實例方法委托,使用方法為
Delegate.CreateDelegate(typeof(Action<TestClass, string>), null, type.GetMethod("TestMethod"))
- 若 firstArgument 為 null,且委托的簽名與方法的簽名匹配,則此時稱為通過空引用封閉的委托,使用方法為
Delegate.CreateDelegate(typeof(Action<string>), null, type.GetMethod("TestMethod"))
將以上六點總結來看,就是根據方法是靜態方法還是實例方法,以及委托與方法簽名的匹配方式就可以決定如何構造委托了。下面就是判斷的流程圖:
圖2 方法委托的流程圖
對於開放的靜態或實例方法,可以使用上一節完成的方法;對於封閉的靜態或實例方法,做法也比較類似,只要將 firstArgument 作為靜態方法的第一個參數或者是實例使用即可;在流程圖中特地將通過空引用封閉的實例方法拿出來,是因為 Expression 不能實現對 null 調用實例方法,只能夠使用 Delegate.CreateDelegate 來生成委托,然后在外面再套一層自己的委托以實現強制類型轉換。這么做效率肯定會更低,但畢竟這種用法基本不可能見到,這里僅僅是為了保證與 CreateDelegate 的統一。
1.3 創建通用的方法委托
這里我多加了一個方法,就是創建一個通用的方法委托,這個委托的聲明如下:
public delegate object MethodInvoker(object instance, params object[] parameters);
通過這個委托,就可以調用任意的方法了。要實現這個方法也很簡單,只要用 Expression 構造出類似於下面的方法即可。
object MethodDelegate(object instance, params object[] parameters) { // 檢查 parameters 的長度。 if (parameters == null || parameters.Length != n + 1) { throw new TargetParameterCountException(); } // 調用方法。 return instance.method((T0)parameters[0], (T1)parameters[1], ... , (Tn)parameters[n]); }
對於泛型方法,顯然無法進行泛型參數推斷,直接報錯就好;對於靜態方法,直接無視 instance 參數就可以。
public static MethodInvoker CreateDelegate(this MethodInfo method) { ExceptionHelper.CheckArgumentNull(method, "method"); if (method.IsGenericMethodDefinition) { // 不對開放的泛型方法執行綁定。 throw ExceptionHelper.BindTargetMethod("method"); } // 要執行方法的實例。 ParameterExpression instanceParam = Expression.Parameter(typeof(object)); // 方法的參數。 ParameterExpression parametersParam = Expression.Parameter(typeof(object[])); // 構造參數列表。 ParameterInfo[] methodParams = method.GetParameters(); Expression[] paramExps = new Expression[methodParams.Length]; for (int i = 0; i < methodParams.Length; i++) { // (Ti)parameters[i] paramExps[i] = ConvertType( Expression.ArrayIndex(parametersParam, Expression.Constant(i)), methodParams[i].ParameterType); } // 靜態方法不需要實例,實例方法需要 (TInstance)instance Expression instanceCast = method.IsStatic ? null : ConvertType(instanceParam, method.DeclaringType); // 調用方法。 Expression methodCall = Expression.Call(instanceCast, method, paramExps); // 添加參數數量檢測。 methodCall = Expression.Block(GetCheckParameterExp(parametersParam, methodParams.Length), methodCall); return Expression.Lambda<MethodInvoker>(GetReturn(methodCall, typeof(object)), instanceParam, parametersParam).Compile(); }
二、從 ConstructorInfo 創建構造函數的委托
創建構造函數的委托的情況就很簡單了,構造函數沒有靜態和實例的區分,不存在泛型方法,而且委托和構造函數的簽名一定是匹配的,實現起來就如同 1.1 創建開放的方法委托,不過這是用到的實 Expression.New 方法而不是 Expression.Call 了。
public static Delegate CreateDelegate(Type type, ConstructorInfo ctor, bool throwOnBindFailure) { ExceptionHelper.CheckArgumentNull(ctor, "ctor"); CheckDelegateType(type, "type"); MethodInfo invoke = type.GetMethod("Invoke"); ParameterInfo[] invokeParams = invoke.GetParameters(); ParameterInfo[] methodParams = ctor.GetParameters(); // 要求參數數量匹配。 if (invokeParams.Length == methodParams.Length) { // 構造函數的參數列表。 ParameterExpression[] paramList = GetParameters(invokeParams); // 構造調用參數列表。 Expression[] paramExps = GetParameterExpressions(paramList, 0, methodParams, 0); if (paramExps != null) { Expression methodCall = Expression.New(ctor, paramExps); methodCall = GetReturn(methodCall, invoke.ReturnType); if (methodCall != null) { return Expression.Lambda(type, methodCall, paramList).Compile(); } } } if (throwOnBindFailure) { throw ExceptionHelper.BindTargetMethod("ctor"); } return null; }
與通用的方法委托類似的,我也使用下面的委托
public delegate object InstanceCreator(params object[] parameters);
來創建通用的構造函數的委托,與通用的方法委托的實現也很類似。
public static Delegate CreateDelegate(Type type, ConstructorInfo ctor, bool throwOnBindFailure) { ExceptionHelper.CheckArgumentNull(ctor, "ctor"); CheckDelegateType(type, "type"); MethodInfo invoke = type.GetMethod("Invoke"); ParameterInfo[] invokeParams = invoke.GetParameters(); ParameterInfo[] methodParams = ctor.GetParameters(); // 要求參數數量匹配。 if (invokeParams.Length == methodParams.Length) { // 構造函數的參數列表。 ParameterExpression[] paramList = GetParameters(invokeParams); // 構造調用參數列表。 Expression[] paramExps = GetParameterExpressions(paramList, 0, methodParams, 0); if (paramExps != null) { Expression methodCall = Expression.New(ctor, paramExps); methodCall = GetReturn(methodCall, invoke.ReturnType); if (methodCall != null) { return Expression.Lambda(type, methodCall, paramList).Compile(); } } } if (throwOnBindFailure) { throw ExceptionHelper.BindTargetMethod("ctor"); } return null; }
三、從 PropertyInfo 創建屬性的委托
有了創建方法的委托作為基礎,創建屬性的委托就非常容易了。如果委托具有返回值那么意味着是獲取屬性,不具有返回值(返回值為 typeof(void))意味着是設置屬性。然后利用 PropertyInfo.GetGetMethod 或 PropertyInfo.GetSetMethod 來獲取相應的 get 訪問器或 set 訪問器,最后直接調用創建方法的委托就可以了。
封閉的屬性委托也同樣很有用,這樣可以將屬性的實例與委托綁定。
對於屬性並沒有創建通用的委托,是因為屬性的訪問分為獲取和設置兩部分的,這兩部分難以有效的結合到一塊。
四、從 FieldInfo 創建字段的委托
在創建字段的委托時,就不能使用現有的方法了,而必須用 Expression.Assign 自己完成字段的賦值。字段的委托同樣可以分為開放的字段委托和使用第一個參數封閉的字段委托,其判斷過程如下:
圖3 字段委托流程圖
字段的處理很簡單,就是通過 Expression.Field 訪問字段,然后通過 Expression.Assign 對字段進行賦值,或者直接返回字段的值。圖中單獨列出來的“通過空引用封閉的實例字段”,同樣是因為不能用代碼訪問空對象的實例字段,這顯然是個毫無意義的操作,不過為了與通過空引用封閉的屬性得到的結果相同,這里總是拋出 System.NullReferenceException。
五、從 Type 創建成員委托
這個方法提供了創建成員委托的最靈活的方式,它可以根據給出的成員名稱、BindingFlags 和委托的簽名決定是創建方法、構造函數、屬性還是字段的委托。
它的做法就是,依次利用 PowerBinder.Cast 在 type 中查找與給定委托簽名匹配的方法、屬性和字段,並嘗試為每個匹配的成員構造委托(使用前面四個部分中給出的方法)。當某個成員成功構造出委托,那么它就是最后需要的那個。
由於 PowerBinder 可以支持查找泛型方法和顯式類型轉換,因此構造委托的時候也自然就能夠支持泛型方法和顯式類型轉換了。
DelegateBuilder 構造委托的方法算是到此結束了,完整的源代碼可見 DelegateBuilder.cs,總共大約 2500 行,不過其中大部分都是注釋和各種方法重載(目前有 54 個重載),VS 代碼度量的結果只有 509 行。