[英]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 : IItem
和Alpha
類不實現此接口的約束)。 但這就是重點。 編譯器沒有此調用有效的信息。 作為程序員的你知道這一點,你應該確保這段代碼運行沒有錯誤。
當您使用動態類型的參數調用非 void 方法時,它的返回類型也可能是dynamic
的。 因此,如果您將前面的示例更改為此代碼:
var result = ProcessItem((dynamic)testObjects[i], "test" + i, i);
那么結果對象的類型將是dynamic
的。 這是因為編譯器並不總是知道將調用哪個方法。 如果您知道函數調用的返回類型,那么您應該將其隱式轉換為所需的類型,以便其余代碼是靜態類型的:
string result = ProcessItem((dynamic)testObjects[i], "test" + i, i);
如果類型不匹配,您將收到運行時錯誤。
實際上,如果您嘗試在前面的示例中獲取結果值,那么您將在第二次循環迭代中遇到運行時錯誤。 這是因為您試圖保存 void 函數的返回值。
從類型信息調用泛型方法涉及三個步驟。
##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 實例,其鍵和值的類型正是在調用keyType
和valueType
時指定的。
這是一個完整的示例,如何調用此方法來實例化和使用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 { }
並且您想使用Bar
和Square
調用方法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.