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

当类型参数在编译时未知,而是在运行时动态获得时,调用泛型方法的最佳方法是什么?

考虑以下示例代码-在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>(){//...}}
334505 次浏览

您需要使用反射来获取要开始的方法,然后通过提供创建泛型方法的类型参数来“构造”它:

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 GenericMethodMethodInfo method = this.GetType().GetMethod(GenMethod.Method.Name);MethodInfo generic = method.MakeGenericMethod(myType);generic.Invoke(this, null);

虽然不是很漂亮,但您在这里有一个对GenericMethod的编译时引用,如果您重构、删除或使用GenericMethod做任何事情,此代码将继续工作,或者至少在编译时中断(例如,如果您删除GenericMethod)。

另一种方法是创建一个新的包装类,并通过Activator创建它。我不知道是否有更好的方法。

使用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}));

通过使用#0类型而不是反射API,可以大大简化调用具有仅在运行时已知的类型参数的泛型方法。

要使用此技术,必须从实际对象(不仅仅是Type类的实例)中知道类型。否则,您必须创建该类型的对象或使用标准反射API解决方案。您可以使用激活器。创建实例方法创建对象。

如果你想调用一个泛型方法,在“正常”使用中会推断出它的类型,那么只需将未知类型的对象转换为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): Alphaitem.GetType(): Beta     typeof(T): Betaitem.GetType(): Alpha    typeof(T): System.Objectitem.GetType(): Beta     typeof(T): System.Objectitem.GetType(): Alpha    typeof(T): Alphaitem.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类不实现这个接口)。但这就是重点。编译器没有这个调用有效的信息。作为程序员,您知道这一点,您应该确保此代码运行时没有错误。

返回类型问题

当你使用动态类型的参数调用一个非空方法时,它的返回类型可能会#0也是。所以如果你将前面的示例更改为此代码:

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

那么结果对象的类型将是dynamic。这是因为编译器并不总是知道将调用哪个方法。如果你知道函数调用的返回类型,那么你应该将其隐式转换到所需的类型,以便其余代码是静态类型的:

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

如果类型不匹配,您将收到运行时错误。

实际上,如果您尝试在前面的示例中获取结果值,那么您将在第二次循环迭代中获得运行时错误。这是因为您尝试保存空函数的返回值。

添加到Adrian Gallero的回答

从类型info调用泛型方法涉及三个步骤。

##TLDR:使用类型对象调用已知的泛型方法可以通过以下方式完成:##

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

其中GenericMethod<object>是要调用的方法名和满足泛型约束的任何类型。

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

##步骤1是获取泛型方法定义的方法信息##

###方法1:使用具有适当类型或绑定标志的Get方法()或Get方法。###

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

###方法2:创建一个委托,获取mododInfo对象,然后调用GetGeneric老兄定义

从包含方法的类内部:

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”或“泛型方法”实际上是指一组可能包含一个或多个方法的方法。在您提供方法参数的类型之前,不知道是哪个方法您正在使用的方法。

((Action)GenericMethod<object>)引用特定方法的委托。((Func<string, int>)GenericMethod<object>)引用不同的泛型方法重载

###方法3:创建一个包含方法调用表达式的lambda表达式,获取mododInfo对象,然后获取GetGeneric老兄定义

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 methodCallExpr = (MethodCallExpression)expr.Body;

从方法中获取泛型方法定义

MethodInfo methodA = methodCallExpr.Method.GetGenericMethodDefinition();

##第2步是调用MakeGenericWay来创建具有适当类型的泛型方法。##

MethodInfo generic = method.MakeGenericMethod(myType);

##第3步是使用适当的参数调用方法。##

generic.Invoke(this, null);

这是我基于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);

然后调用GetWay来查找您的泛型函数:

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

到目前为止,上面的调用与上面解释的几乎相同,但当您需要向其传递多个参数时,有一点不同。

您需要将Type[]数组传递给MakeGenericWay函数,该函数包含上面创建的“虚拟”对象类型:

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

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

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

你完蛋了。很有魅力!

更新:

正如@Bevan强调的那样,在调用MakeGenericWay函数时,我不需要创建一个数组,因为它接受参数,而且我不需要创建一个对象来获取类型,因为我可以直接将类型传递给这个函数。在我的情况下,由于我在另一个类中预定义了类型,我只需将代码更改为:

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的属性,我在运行时根据传递给构造函数的枚举值设置这些属性,并将为我提供相关类型,然后我在MakeGeneric方法中使用这些类型。

再次感谢您突出显示此@Bevan。

没有人提供“经典反射”解决方案,所以这里有一个完整的代码示例:

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)

它创建并返回一个IDicpedia实例,其键和值的类型与调用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);}}elseConsole.WriteLine("null");}}}

当执行上述控制台应用程序时,我们会得到正确的预期结果:

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

神秘的答案的启发-假设你有两个(或更多)类,比如

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();

它适用于每个类。在这种情况下,它将输出:

正方形
酒吧