C# 反射的委託建立器

來源:互聯網
上載者:User
文章目錄
  • 1.1 建立開放的方法委託
  • 1.2 建立第一個參數封閉的方法委託
  • 1.3 建立通用的方法委託

.Net 的反射是個很好很強大的東西,不過它的效率卻實在是不給力。已經有很多人針對這個問題討論過了,包括各種各樣的 DynamicMethod 和各種各樣的效率測試,不過總的來說解決方案就是利用 Expression Tree、Delegate.CreateDelegate 或者 Emit 構造出反射操作對應的委託,從而實現加速反射的目的。

雖然本篇文章同樣是討論利用委託來加速反射調用函數,不過重點並不在於如何提升調用速度,而是如何更加智能的構造出反射的委託,並最終完成一個方便易用的委託建立器 DelegateBuilder。

它的設計目標是:

  1. 能夠對方法調用、建構函式調用,擷取或設定屬性和擷取或設定欄位提供支援。
  2. 能夠構造出特定的委託類型,而不僅限於 Func<object, object[], object> 或者其它的 Func 和 Action,因為我個人很喜歡強型別的委託,同時類似 void MyDeleagte(params int[] args) 這樣的委託有時候也是很有必要的,如果需要支援 ref 和 out 參數,就必須使用自訂的委託類型了。
  3. 能夠支援泛型方法,因為利用反射選擇泛型方法是件很糾結的事(除非沒有同名方法),而且還需要再 MakeGenericMethod。
  4. 能夠支援類型的顯式轉換,在對某些 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 (靜態方法)來說:

  1. 若 firstArgument 不為 null,則在每次調用委託時將其傳遞給方法的第一個參數,此時稱為通過第一個參數封閉,要求委託的簽名包括方法除第一個參數之外的所有參數,使用方法為

    Delegate.CreateDelegate(typeof(Action), "str", type.GetMethod("TestStaticMethod"))

    得到的委託的效果與 TestStaticMethod(firstArgument) 相同。

  2. 若 firstArgument 為 null,且委託和方法的簽名匹配(即所有參數類型都相容),則此時稱為開放的靜態方法委託,使用方法為
    Delegate.CreateDelegate(typeof(Action<string>), null, type.GetMethod("TestStaticMethod"))

    得到的委託的效果與 TestStaticMethod(arg1) 相同。

  3. 若 firstArgument 為 null,且委託的簽名以方法的第二個參數開頭,其餘參數類型都相容,則此時稱為通過Null 參考封閉的委託,使用方法為
    Delegate.CreateDelegate(typeof(Action), null, type.GetMethod("TestStaticMethod"))

    得到的委託的效果與 TestStaticMethod(null) 相同。

對於 TestMethod (執行個體方法)來說:

  1. 若 firstArgument 不為 null,則 firstArgument 被傳遞給隱藏的執行個體參數(就是 this),這時成為封閉的執行個體方法,要求委託的簽名必須和方法的簽名匹配,使用方法為

    Delegate.CreateDelegate(typeof(Action<string>), new TestClass(), type.GetMethod("TestMethod"))

    得到的委託效果與 firstArgument.TestMethod(arg1) 相同。

  2. 若 firstArgument 為 null,且委託顯示包含方法的第一個隱藏參數(就是 this),則此時稱為開放的執行個體方法委託,使用方法為
    Delegate.CreateDelegate(typeof(Action<TestClass, string>), null, type.GetMethod("TestMethod"))

    得到的委託效果與 arg1.TestMethod(arg2) 相同。

  3. 若 firstArgument 為 null,且委託的簽名與方法的簽名匹配,則此時稱為通過Null 參考封閉的委託,使用方法為
    Delegate.CreateDelegate(typeof(Action<string>), null, type.GetMethod("TestMethod"))

    這種用法比較奇怪,這種用法類似於對空執行個體調用執行個體方法(null.TestMethod(obj)),在方法體內得到的 this 就是 null,在實際當中不是很有用。

將以上六點總結來看,就是根據方法是靜態方法還是執行個體方法,以及委託與方法簽名的匹配方式就可以決定如何構造委託了。下面就是判斷的流程圖:

圖2 方法委託的流程圖

對於開放的靜態或執行個體方法,可以使用上一節完成的方法;對於封閉的靜態或執行個體方法,做法也比較類似,只要將 firstArgument 作為靜態方法的第一個參數或者是執行個體使用即可;在流程圖中特地將通過Null 參考封閉的執行個體方法拿出來,是因為 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)instanceExpression 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 對欄位進行賦值,或者直接返回欄位的值。圖中單獨列出來的“通過Null 參考封閉的執行個體欄位”,同樣是因為不能用代碼訪問Null 物件的執行個體欄位,這顯然是個毫無意義的操作,不過為了與通過Null 參考封閉的屬性得到的結果相同,這裡總是拋出 System.NullReferenceException。

五、從 Type 建立成員委派

這個方法提供了建立成員委派的最靈活的方式,它可以根據給出的成員名稱、BindingFlags 和委託的簽名決定是建立方法、建構函式、屬性還是欄位的委託。

它的做法就是,依次利用 PowerBinder.Cast 在 type 中尋找與給定委託簽名匹配的方法、屬性和欄位,並嘗試為每個匹配的成員構造委託(使用前面四個部分中給出的方法)。當某個成員成功構造出委託,那麼它就是最後需要的那個。

由於 PowerBinder 可以支援尋找泛型方法和顯式類型轉換,因此構造委託的時候也自然就能夠支援泛型方法和顯式類型轉換了。

DelegateBuilder 構造委託的方法算是到此結束了,完整的原始碼可見 DelegateBuilder.cs,總共大約 2500 行,不過其中大部分都是注釋和各種方法重載(目前有 54 個重載),VS 程式碼度量的結果只有 509 行。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.