Reflection-使用反射執行方法的7種方式

Reflection-使用反射執行方法的7種方式

最近因為專案需求,努力寫著反射(Reflection)程式,還好哥早在2011年就打過底,但大型實務應用開發之後在 fb 上發表些小心得:

  • 取得物件 System.Type 後基本上就無敵了。
  • 你能做的、你不能做的,Reflection 都能做。白話:一些簡單的 private 之類限制對 Reflection 無效。
  • 製作沒有規範 Assembly 的 Reflection 解析執行程式需要:細心、耐心與草x馬的愛心。(突然覺得那些反編譯器好偉大)
  • 接上題,規範好 Assembly 介面定義,大家都會很開心
  • 寫法不同,效果不同。純反射 4.1 > dynamic 1.4 < delegate 1.2。delegate 最快。
  • Expression 網路上的資料它也比 dynamic 快,但我的實作不適合驗證,沒取得數值。
  • ps. 美國微軟工程師的 Bruce 說,dynamic 能不用就不用。我自己的實作心得:dynamic 沒有比 delegate 快,又有些缺點,我也不建議使用。

後來,實在手癢的受不了,把底層用 Expression 改寫,得到以下結果:

  • v1, reflection - 純反射,最慢;
  • v2, delegate - 比純反射快 4 倍(自己測);
  • v3, expression - 與 delegate 差不多;

速度與可讀性而言,目前 delegate 最好。寫法不同,效果不同。

Reflection - Method Invoke 7 Ways

這裡我將手邊知道的「執行方法」的方法做了以下整理,總共有 7 種方向。

    /// <summary>
    /// 受測類別
    /// </summary>
    public class MyClass
    {
        /// <summary>
        /// Foo 受測方法
        /// </summary>
        /// <param name="value">int 型別值</param>
        /// <returns></returns>
        public string Foo(int value)
        {
            return $"Foo value: {value}";
        }

        /// <summary>
        /// Bar 受測方法
        /// </summary>
        /// <param name="value">string 型別值</param>
        /// <returns></returns>
        public string Bar(string value)
        {
            return $"Bar value: {value}";
        }
    }
 

Way1:物件直接進行方法呼叫。

這應該是大多數人天天都在使用的方式。把物件 new 起來,直接呼叫。

    /// <summary>
    /// Way1:物件直接進行方法呼叫。
    /// </summary>
    /// <param name="targetObject">The target object.</param>
    internal void Way1_DirectMethodCall(object targetObject)
    {
        var baseName = MethodBase.GetCurrentMethod().Name;
        var myClass = ((MyClass)targetObject);
        var fooResult = myClass.Foo(1);
        var barResult = myClass.Bar("Bruce");
        PrintResult(baseName, fooResult, barResult);
    }

    [Conditional("DEBUG")]
    private void PrintResult(string baseName, string foo, string bar)
    {
        Console.WriteLine($"{baseName}: {foo}, {bar}");
    }
    

Way2:建立 lambda 並進行方法呼叫。

透過 Action<> 或 Func<> 建立委派,然後進行呼叫。

    /// <summary>
    /// Way2:建立 lambda 並進行方法呼叫。
    /// </summary>
    /// <param name="targetObject">The target object.</param>
    internal void Way2_CreateALambdaCall(object targetObject)
    {
        var baseName = MethodBase.GetCurrentMethod().Name;

        Func<int, string> fooFunc = f => ((MyClass)targetObject).Foo(f);
        var fooResult = fooFunc(2);

        Func<string, string> barFunc = b => ((MyClass)targetObject).Bar(b);
        var barResult = barFunc("Sherry");

        PrintResult(baseName, fooResult, barResult);
    }
 

Way3:使用純反射(反映)的 Method.Invoke 進行方法呼叫

使用 Reflection API 的 Method.Invoke 來進行方法呼叫,這也是反射最入門的使用方式。

    /// <summary>
    /// Way3:使用純反射(反映)的 Method.Invoke 進行方法呼叫
    /// </summary>
    /// <param name="targetObject">The target object.</param>
    internal void Way3_UsingMethodInvoke(object targetObject)
    {
        var baseName = MethodBase.GetCurrentMethod().Name;

        var foo = targetObject.GetType().GetMethod("Foo");
        var fooResult = foo.Invoke(targetObject, new object[] { 3 }) as string;

        var bar = targetObject.GetType().GetMethod("Bar");
        var barResult = bar.Invoke(targetObject, new object[] { "Happy" }) as string;

        PrintResult(baseName, fooResult, barResult);
    }
 

Way4:使用 dynamic 關鍵字。

dynamic 缺點很明顯,就是失去強型別效果,但速度不差,也因為失去強型別,撰寫程式碼時不會有 IntelliSense 提示,例如,.Foo(4).Bar("Love")

    /// <summary>
    /// Way4:使用 dynamic 關鍵字。
    /// </summary>
    /// <param name="targetObject">The target object.</param>
    internal void Way4_Using_dynamic_Keyword(object targetObject)
    {
        var baseName = MethodBase.GetCurrentMethod().Name;
        var dynamicTarget = (dynamic)targetObject;
        var fooResult = dynamicTarget.Foo(4) as string;
        var barResult = dynamicTarget.Bar("Love") as string;
        PrintResult(baseName, fooResult, barResult);
    }
 

Way5:使用 Delegate.CreateDelegate 建立委派並呼叫方法。

結合 Action<> 或 Func<> 與 Delegate.CreateDelegate 建立委派並執行方法。

    /// <summary>
    /// Way5:使用 Delegate.CreateDelegate 建立委派並呼叫方法。
    /// </summary>
    /// <param name="targetObject">The target object.</param>
    internal void Way5_CreateDelegateCall(object targetObject)
    {
        var baseName = MethodBase.GetCurrentMethod().Name;
        var fooMethod = targetObject.GetType().GetMethod("Foo");
        var fooFunc = (Func<int, string>)Delegate.CreateDelegate(typeof(Func<int, string>), targetObject, fooMethod);
        var fooResult = fooFunc(5);

        var barMethod = targetObject.GetType().GetMethod("Bar");
        var barFunc = (Func<string, string>)Delegate.CreateDelegate(typeof(Func<string, string>), targetObject, barMethod);
        var barResult = barFunc("Gina");

        PrintResult(baseName, fooResult, barResult);
    }
 

Way6: 建立 Expression 並進行方法呼叫。

使用 Expression 建立運算式並呼叫方法。

    /// <summary>
    /// Way6: 建立 expression 並進行方法呼叫。
    /// </summary>
    /// <param name="targetObject">The target object.</param>
    internal void Way6_CreateExpressionCall(object targetObject)
    {
        var baseName = MethodBase.GetCurrentMethod().Name;
        var thisObject = Expression.Constant(targetObject);

        var fooMethod = targetObject.GetType().GetMethod("Foo");
        var intValue = Expression.Parameter(typeof(int), "value");
        var fooCall = Expression.Call(thisObject, fooMethod, intValue);
        var fooLambda = Expression.Lambda<Func<int, string>>(fooCall, intValue);
        var fooFunc = fooLambda.Compile();
        var fooResult = fooFunc(6);

        var barMethod = targetObject.GetType().GetMethod("Bar");
        var strValue = Expression.Parameter(typeof(string), "value");
        var barCall = Expression.Call(thisObject, barMethod, strValue);
        var barLambda = Expression.Lambda<Func<string, string>>(barCall, strValue);
        var barFunc = barLambda.Compile();
        var barResult = barFunc("Metilda");

        PrintResult(baseName, fooResult, barResult);
    }
 

Way7:使用 DynamicMethod 動態產生 IL Code

這大概是所有反射中最難寫與最沒可讀性的,但也是功能最強大的 API。

    /// <summary>
    /// Way7: 使用 Reflection.Emit.DynamicMethod 動態產生 IL 並透過 CreateDelegate 建立 Func 委派執行方法。
    /// </summary>
    /// <param name="targetObject">The target object.</param>
    internal void Way7_UsingDynamicMethodCall(object targetObject)
    {
        var baseName = MethodBase.GetCurrentMethod().Name;
        var type = targetObject.GetType();

        var fooMethod = type.GetMethod("Foo");

        var fooDynamicMethod = new DynamicMethod("Foo_",
            typeof(string),
            new[] { type, typeof(int) },
            true);
        var fooIl = fooDynamicMethod.GetILGenerator();
        fooIl.DeclareLocal(typeof(string));
        fooIl.Emit(OpCodes.Ldarg_0);
        fooIl.Emit(OpCodes.Ldarg_1);
        fooIl.Emit(OpCodes.Call, fooMethod);
        fooIl.Emit(OpCodes.Ret);

        var fooFunc = (Func<int, string>)fooDynamicMethod.CreateDelegate(typeof(Func<int, string>), targetObject);
        var fooResult = fooFunc(7);

        var barMethod = type.GetMethod("Bar");
        var barDynamicMethod = new DynamicMethod("Bar_",
            typeof(string),
            new[] { type, typeof(string) },
            true);
        var barIl = barDynamicMethod.GetILGenerator();
        barIl.DeclareLocal(typeof(string));
        barIl.Emit(OpCodes.Ldarg_0);
        barIl.Emit(OpCodes.Ldarg_1);
        barIl.Emit(OpCodes.Call, barMethod);
        barIl.Emit(OpCodes.Ret);

        var barFunc = (Func<string, string>)barDynamicMethod.CreateDelegate(typeof(Func<string, string>), targetObject);
        var barResult = barFunc("Nancy");

        PrintResult(baseName, fooResult, barResult);
    }
 
method invoke 7 ways

小結

瞭解反射的運作的好處是,更加明白物件在底層運作的流程作業。但好好的沒事搞那麼多種做什麼,無聊的人(指我)就會開始寫個程式開始測時間,後來發現,我錯了,「寫法不同,效果不同。」(第三次)有其意義,而意義絕不是寫個for迴圈那麼簡單。

下集:還在徒手揮汗寫For測效能,閃開讓BenchmarkDotNet來

4 則留言:

  1. 請問WAY6: 建立 EXPRESSION 並進行方法呼叫 如果是 void 沒有回傳資料 要怎麼修改呢? 謝謝

    回覆刪除
    回覆
    1. 沒有回傳值,不是Action<>嗎!

      刪除
    2. 我的method name; type 都是動態的(from string), 所以最後只能用DynamicInvoke , 這有點慢, 請問有解法嗎? 謝謝

      var methodContent = JsonConvert.DeserializeObject(msg.CMD);
      var serviceType = Type.GetType(Type.GetType(methodContent.ServiceTypeName).AssemblyQualifiedName);
      object service = serviceProvider.GetRequiredService(serviceType);

      var fooMethod = service.GetType().GetMethod(methodContent.MethodName);

      if (methodContent.IsGenericMethod)
      {
      var genType = Type.GetType(Type.GetType(methodContent.MethodGeneircTypeName).AssemblyQualifiedName);
      fooMethod = fooMethod.MakeGenericMethod(genType);
      }


      var inputType = Type.GetType(Type.GetType(methodContent.MethodInputParamTypeName).AssemblyQualifiedName);

      Type delegateType;
      Type outputType;
      if (methodContent.isVoid)
      {
      delegateType = typeof(Action<>).MakeGenericType(inputType);
      }
      else
      {
      outputType = Type.GetType(Type.GetType(methodContent.MethodOutputParamTypeName).AssemblyQualifiedName);
      delegateType = typeof(Func<,>).MakeGenericType(inputType, outputType);
      }

      var fooFunc = Delegate.CreateDelegate(delegateType, service, fooMethod);

      var output = fooFunc.DynamicInvoke(JsonConvert.DeserializeObject(msg.BDY, inputType));

      刪除

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。