簡體   English   中英

如何使用反射調用泛型方法?

[英]How do I use reflection to call a generic method?

當類型參數在編譯時未知,而是在運行時動態獲取時,調用泛型方法的最佳方法是什么?

考慮以下示例代碼 - 在Example()方法內,使用存儲在myType變量中的Type調用GenericMethod<T>()的最簡潔方法是什么?

public class Sample
{
    public void Example(string typeName)
    {
        Type myType = FindType(typeName);

        // What goes here to call GenericMethod<T>()?
        GenericMethod<myType>(); // This doesn't work

        // What changes to call StaticMethod<T>()?
        Sample.StaticMethod<myType>(); // This also doesn't work
    }

    public void GenericMethod<T>()
    {
        // ...
    }

    public static void StaticMethod<T>()
    {
        //...
    }
}

您需要使用反射來獲取方法,然后通過使用MakeGenericMethod提供類型參數來“構造”它:

MethodInfo method = typeof(Sample).GetMethod(nameof(Sample.GenericMethod));
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

對於靜態方法,將null作為第一個參數傳遞給Invoke 這與泛型方法無關——它只是正常的反射。

如前所述,從使用dynamic的 C# 4 開始,其中很多內容都更簡單——當然,如果您可以使用類型推斷。 在類型推斷不可用的情況下(例如問題中的確切示例),它沒有幫助。

只是對原始答案的補充。 雖然這會起作用:

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

這也有點危險,因為您丟失了GenericMethod的編譯時檢查。 如果您稍后進行重構並重命名GenericMethod ,此代碼將不會注意到並且會在運行時失敗。 此外,如果對程序集進行任何后處理(例如混淆或刪除未使用的方法/類),此代碼也可能會中斷。

因此,如果您知道在編譯時鏈接到的方法,並且這不會被調用數百萬次,因此開銷無關緊要,我會將此代碼更改為:

Action<> GenMethod = GenericMethod<int>;  //change int by any base type 
                                          //accepted by GenericMethod
MethodInfo method = this.GetType().GetMethod(GenMethod.Method.Name);
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

雖然不是很漂亮,但您在這里有一個對GenericMethod的編譯時引用,如果您重構、刪除或對GenericMethod執行任何操作,此代碼將繼續工作,或者至少在編譯時中斷(例如,如果您刪除GenericMethod )。

其他方法是創建一個新的包裝類,並通過Activator創建它。 不知道有沒有更好的辦法。

使用僅在運行時已知的類型參數調用泛型方法可以通過使用dynamic類型而不是反射 API 來大大簡化。

要使用這種技術,必須從實際對象(不僅僅是Type類的實例)中知道類型。 否則,您必須創建該類型的對象或使用標准反射 API解決方案 您可以使用Activator.CreateInstance方法創建對象。

如果你想調用一個泛型方法,在“正常”使用中會推斷出它的類型,那么它只是將未知類型的對象轉換為dynamic 這是一個例子:

class Alpha { }
class Beta { }
class Service
{
    public void Process<T>(T item)
    {
        Console.WriteLine("item.GetType(): " + item.GetType()
                          + "\ttypeof(T): " + typeof(T));
    }
}

class Program
{
    static void Main(string[] args)
    {
        var a = new Alpha();
        var b = new Beta();

        var service = new Service();
        service.Process(a); // Same as "service.Process<Alpha>(a)"
        service.Process(b); // Same as "service.Process<Beta>(b)"

        var objects = new object[] { a, b };
        foreach (var o in objects)
        {
            service.Process(o); // Same as "service.Process<object>(o)"
        }
        foreach (var o in objects)
        {
            dynamic dynObj = o;
            service.Process(dynObj); // Or write "service.Process((dynamic)o)"
        }
    }
}

這是這個程序的輸出:

item.GetType(): Alpha    typeof(T): Alpha
item.GetType(): Beta     typeof(T): Beta
item.GetType(): Alpha    typeof(T): System.Object
item.GetType(): Beta     typeof(T): System.Object
item.GetType(): Alpha    typeof(T): Alpha
item.GetType(): Beta     typeof(T): Beta

Process是一個泛型實例方法,它寫入傳遞參數的真實類型(通過使用GetType()方法)和泛型參數的類型(通過使用typeof運算符)。

通過將對象參數轉換為dynamic類型,我們將類型參數的提供推遲到運行時。 當使用dynamic參數調用Process方法時,編譯器不關心此參數的類型。 編譯器生成代碼,在運行時檢查傳遞參數的真實類型(通過使用反射)並選擇最佳調用方法。 這里只有一個泛型方法,因此使用適當的類型參數調用它。

在此示例中,輸出與您編寫的相同:

foreach (var o in objects)
{
    MethodInfo method = typeof(Service).GetMethod("Process");
    MethodInfo generic = method.MakeGenericMethod(o.GetType());
    generic.Invoke(service, new object[] { o });
}

動態類型的版本肯定更短更容易編寫。 您也不應該擔心多次調用此函數的性能。 由於 DLR 中的緩存機制,下一次使用相同類型參數的調用應該會更快。 當然,您可以編寫緩存調用的委托的代碼,但是通過使用dynamic類型,您可以免費獲得這種行為。

如果您要調用的泛型方法沒有參數化類型的參數(因此無法推斷其類型參數),那么您可以將泛型方法的調用包裝在輔助方法中,如下例所示:

class Program
{
    static void Main(string[] args)
    {
        object obj = new Alpha();

        Helper((dynamic)obj);
    }

    public static void Helper<T>(T obj)
    {
        GenericMethod<T>();
    }

    public static void GenericMethod<T>()
    {
        Console.WriteLine("GenericMethod<" + typeof(T) + ">");
    }
}

提高類型安全性

使用dynamic對象代替反射 API 的真正好處在於,您只會丟失對這種特定類型的編譯時檢查,而這種檢查直到運行時才知道。 編譯器照常靜態分析其他參數和方法名稱。 如果您刪除或添加更多參數、更改它們的類型或重命名方法名稱,那么您將收到編譯時錯誤。 如果您在Type.GetMethod中將方法名稱作為字符串提供,在MethodInfo.Invoke中將參數作為對象數組提供,則不會發生這種情況。

下面是一個簡單的示例,說明了如何在編譯時(注釋代碼)和運行時捕獲其他錯誤。 它還顯示了 DLR 如何嘗試解析要調用的方法。

interface IItem { }
class FooItem : IItem { }
class BarItem : IItem { }
class Alpha { }

class Program
{
    static void Main(string[] args)
    {
        var objects = new object[] { new FooItem(), new BarItem(), new Alpha() };
        for (int i = 0; i < objects.Length; i++)
        {
            ProcessItem((dynamic)objects[i], "test" + i, i);

            //ProcesItm((dynamic)objects[i], "test" + i, i);
            //compiler error: The name 'ProcesItm' does not
            //exist in the current context

            //ProcessItem((dynamic)objects[i], "test" + i);
            //error: No overload for method 'ProcessItem' takes 2 arguments
        }
    }

    static string ProcessItem<T>(T item, string text, int number)
        where T : IItem
    {
        Console.WriteLine("Generic ProcessItem<{0}>, text {1}, number:{2}",
                          typeof(T), text, number);
        return "OK";
    }
    static void ProcessItem(BarItem item, string text, int number)
    {
        Console.WriteLine("ProcessItem with Bar, " + text + ", " + number);
    }
}

在這里,我們再次通過將參數轉換為dynamic類型來執行某些方法。 只有第一個參數類型的驗證被推遲到運行時。 如果您正在調用的方法的名稱不存在或其他參數無效(參數數量錯誤或類型錯誤),您將收到編譯器錯誤。

當您將dynamic參數傳遞給方法時,此調用是最近綁定的 方法重載解析發生在運行時並嘗試選擇最佳重載。 因此,如果您使用BarItem類型的對象調用ProcessItem方法,那么您實際上將調用非泛型方法,因為它更適合這種類型。 但是,當您傳遞Alpha類型的參數時會出現運行時錯誤,因為沒有可以處理此對象的方法(通用方法具有where T : IItemAlpha類不實現此接口的約束)。 但這就是重點。 編譯器沒有此調用有效的信息。 作為程序員的你知道這一點,你應該確保這段代碼運行沒有錯誤。

返回類型陷阱

當您使用動態類型的參數調用非 void 方法時,它的返回類型也可能dynamic的。 因此,如果您將前面的示例更改為此代碼:

var result = ProcessItem((dynamic)testObjects[i], "test" + i, i);

那么結果對象的類型將是dynamic的。 這是因為編譯器並不總是知道將調用哪個方法。 如果您知道函數調用的返回類型,那么您應該將其隱式轉換為所需的類型,以便其余代碼是靜態類型的:

string result = ProcessItem((dynamic)testObjects[i], "test" + i, i);

如果類型不匹配,您將收到運行時錯誤。

實際上,如果您嘗試在前面的示例中獲取結果值,那么您將在第二次循環迭代中遇到運行時錯誤。 這是因為您試圖保存 void 函數的返回值。

添加到Adrian Gallero 的答案

從類型信息調用泛型方法涉及三個步驟。

##TLDR:使用類型對象調用已知的泛型方法可以通過以下方式完成:##

((Action)GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition()
    .MakeGenericMethod(typeof(string))
    .Invoke(this, null);

其中GenericMethod<object>是要調用的方法名稱和滿足通用約束的任何類型。

(Action) 匹配要調用的方法的簽名,即( Func<string,string,int>Action<bool>

##Step 1 正在獲取泛型方法定義的 MethodInfo##

###Method 1:使用帶有適當類型或綁定標志的 GetMethod() 或 GetMethods()。###

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");

###Method 2:創建委托,獲取MethodInfo對象,然后調用GetGenericMethodDefinition

從包含方法的類內部:

MethodInfo method = ((Action)GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition();

MethodInfo method = ((Action)StaticMethod<object>)
    .Method
    .GetGenericMethodDefinition();

從包含方法的類外部:

MethodInfo method = ((Action)(new Sample())
    .GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition();

MethodInfo method = ((Action)Sample.StaticMethod<object>)
    .Method
    .GetGenericMethodDefinition();

在C#中,方法的名稱,即“ToString”或“GenericMethod”實際上是指一組方法,其中可能包含一個或多個方法。 在您提供方法參數的類型之前,不知道您指的是哪種方法。

((Action)GenericMethod<object>)指的是特定方法的委托。 ((Func<string, int>)GenericMethod<object>)指的是不同的 GenericMethod 重載

###Method 3:創建一個包含方法調用表達式的lambda表達式,獲取MethodInfo對象然后GetGenericMethodDefinition

MethodInfo method = ((MethodCallExpression)((Expression<Action<Sample>>)(
    (Sample v) => v.GenericMethod<object>()
    )).Body).Method.GetGenericMethodDefinition();

這分解為

創建一個 lambda 表達式,其中主體是對所需方法的調用。

Expression<Action<Sample>> expr = (Sample v) => v.GenericMethod<object>();

提取主體並轉換為 MethodCallExpression

MethodCallExpression methodCallExpr = (MethodCallExpression)expr.Body;

從方法中獲取泛型方法定義

MethodInfo methodA = methodCallExpr.Method.GetGenericMethodDefinition();

##Step 2 正在調用 MakeGenericMethod 以創建具有適當類型的泛型方法。##

MethodInfo generic = method.MakeGenericMethod(myType);

##Step 3 使用適當的參數調用方法。##

generic.Invoke(this, null);

在 C# 4.0 中,不需要反射,因為 DLR 可以使用運行時類型調用它。 由於動態使用 DLR 庫有點痛苦(而不是 C# 編譯器為您生成代碼),開源框架Dynamitey (.net 標准 1.5)為您提供了對編譯器將生成的相同調用的輕松緩存運行時訪問為你。

var name = InvokeMemberName.Create;
Dynamic.InvokeMemberAction(this, name("GenericMethod", new[]{myType}));


var staticContext = InvokeContext.CreateStatic;
Dynamic.InvokeMemberAction(staticContext(typeof(Sample)), name("StaticMethod", new[]{myType}));

沒有人提供“經典反射”的解決方案,所以這里是一個完整的代碼示例:

using System;
using System.Collections;
using System.Collections.Generic;

namespace DictionaryRuntime
{
    public class DynamicDictionaryFactory
    {
        /// <summary>
        /// Factory to create dynamically a generic Dictionary.
        /// </summary>
        public IDictionary CreateDynamicGenericInstance(Type keyType, Type valueType)
        {
            //Creating the Dictionary.
            Type typeDict = typeof(Dictionary<,>);

            //Creating KeyValue Type for Dictionary.
            Type[] typeArgs = { keyType, valueType };

            //Passing the Type and create Dictionary Type.
            Type genericType = typeDict.MakeGenericType(typeArgs);

            //Creating Instance for Dictionary<K,T>.
            IDictionary d = Activator.CreateInstance(genericType) as IDictionary;

            return d;

        }
    }
}

上面的DynamicDictionaryFactory類有一個方法

CreateDynamicGenericInstance(Type keyType, Type valueType)

它創建並返回一個 IDictionary 實例,其鍵和值的類型正是在調用keyTypevalueType時指定的。

這是一個完整的示例,如何調用此方法來實例化和使用Dictionary<String, int>

using System;
using System.Collections.Generic;

namespace DynamicDictionary
{
    class Test
    {
        static void Main(string[] args)
        {
            var factory = new DictionaryRuntime.DynamicDictionaryFactory();
            var dict = factory.CreateDynamicGenericInstance(typeof(String), typeof(int));

            var typedDict = dict as Dictionary<String, int>;

            if (typedDict != null)
            {
                Console.WriteLine("Dictionary<String, int>");

                typedDict.Add("One", 1);
                typedDict.Add("Two", 2);
                typedDict.Add("Three", 3);

                foreach(var kvp in typedDict)
                {
                    Console.WriteLine("\"" + kvp.Key + "\": " + kvp.Value);
                }
            }
            else
                Console.WriteLine("null");
        }
    }
}

當執行上述控制台應用程序時,我們得到了正確的預期結果:

Dictionary<String, int>
"One": 1
"Two": 2
"Three": 3

這是基於Grax 的答案的我的 2 美分,但通用方法需要兩個參數。

假設您的方法在 Helpers 類中定義如下:

public class Helpers
{
    public static U ConvertCsvDataToCollection<U, T>(string csvData)
    where U : ObservableCollection<T>
    {
      //transform code here
    }
}

在我的例子中,U 類型始終是存儲 T 類型對象的可觀察集合。

由於我已經預定義了類型,我首先創建了代表可觀察集合 (U) 和存儲在其中的對象 (T) 的“虛擬”對象,這些對象將在下面調用 Make 時用於獲取它們的類型

object myCollection = Activator.CreateInstance(collectionType);
object myoObject = Activator.CreateInstance(objectType);

然后調用 GetMethod 來查找您的通用函數:

MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");

到目前為止,上面的調用與上面解釋的幾乎相同,但是當你需要向它傳遞多個參數時會有一點不同。

您需要將 Type[] 數組傳遞給包含上面創建的“虛擬”對象類型的 MakeGenericMethod 函數:

MethodInfo generic = method.MakeGenericMethod(
new Type[] {
   myCollection.GetType(),
   myObject.GetType()
});

完成后,您需要調用上面提到的 Invoke 方法。

generic.Invoke(null, new object[] { csvData });

你完成了。 很有魅力!

更新:

正如@Bevan 強調的那樣,在調用 MakeGenericMethod 函數時我不需要創建數組,因為它需要參數,我不需要創建對象來獲取類型,因為我可以直接將類型傳遞給這個函數。 就我而言,由於我在另一個類中預定義了類型,我只是將代碼更改為:

object myCollection = null;

MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");

MethodInfo generic = method.MakeGenericMethod(
   myClassInfo.CollectionType,
   myClassInfo.ObjectType
);

myCollection = generic.Invoke(null, new object[] { csvData });

myClassInfo 包含 2 個Type的屬性,我在運行時根據傳遞給構造函數的枚舉值設置它們,並將為我提供相關類型,然后我將在 MakeGenericMethod 中使用這些類型。

再次感謝您強調此@Bevan。

Enigmativity 回答的啟發 - 假設您有兩個(或更多)課程,例如

public class Bar { }
public class Square { }

並且您想使用BarSquare調用方法Foo<T> ,該方法聲明為

public class myClass
{
    public void Foo<T>(T item)
    {
        Console.WriteLine(typeof(T).Name);
    }
}

然后你可以實現一個擴展方法,如:

public static class Extension
{
    public static void InvokeFoo<T>(this T t)
    {
        var fooMethod = typeof(myClass).GetMethod("Foo");
        var tType = typeof(T);
        var fooTMethod = fooMethod.MakeGenericMethod(new[] { tType });
        fooTMethod.Invoke(new myClass(), new object[] { t });
    }
}

有了這個,你可以簡單地調用Foo像:

var objSquare = new Square();
objSquare.InvokeFoo();

var objBar = new Bar();
objBar.InvokeFoo();

適用於每個班級。 在這種情況下,它將輸出:

正方形
酒吧

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM