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

Posted

技术标签:

【中文标题】如何使用反射来调用泛型方法?【英文标题】:How do I use reflection to call a generic method? 【发布时间】:2008-10-24 05:17:35 【问题描述】:

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

考虑以下示例代码 - 在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>()
    
        //...
    

【问题讨论】:

我尝试了 Jon 的解决方案,但直到我在课堂上公开了泛型方法后才能让它工作。我知道另一个 Jon 回复说您需要指定绑定标志,但这没有帮助。 你还需要BindingFlags.Instance,而不仅仅是BindingFlags.NonPublic,来获取私有/内部方法。 这个问题的现代版:***.com/q/2433436/103167 @Peter Mortensen - 仅供参考,我在“?”之前使用了空格将英语部分与非英语 (C#) 部分分开;恕我直言,删除空间使其看起来像?是代码的一部分。如果没有代码,我当然同意删除空格,但在这种情况下...... 我们可以定义一个泛型方法,然后使用GetMethod方法获取泛型方法的所有信息并使用。 【参考方案1】:

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

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

对于静态方法,将null 作为第一个参数传递给Invoke。这与泛型方法无关——它只是正常的反射。

如前所述,从 C# 4 开始,使用dynamic 时,其中的大部分内容都更简单——当然,如果您可以使用类型推断。它在类型推断不可用的情况下没有帮助,例如问题中的确切示例。

【讨论】:

+1;注意GetMethod()默认只考虑公共实例方法,所以你可能需要BindingFlags.Static和/或BindingFlags.NonPublic 正确的标志组合是BindingFlags.NonPublic | BindingFlags.Instance(也可以选择BindingFlags.Static)。 一个被标记为欺骗的问题想知道如何使用静态方法来做到这一点 - 从技术上讲,这里的问题也是如此。 generic.Invoke() 的第一个参数在调用静态方法时应该为 null。第一个参数只有在调用实例方法时才需要。 @ChrisMoschini:将其添加到答案中。 @gzou:我在答案中添加了一些内容 - 但请注意,对于调用泛型方法在问题中dynamic 没有帮助,因为类型推断不是不可用。 (编译器无法使用任何参数来确定类型参数。)【参考方案2】:

只是对原始答案的补充。虽然这会起作用:

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 创建它。不知道有没有更好的办法。

【讨论】:

在使用反射调用方法的情况下,通常方法名本身会被另一个方法发现。事先知道方法名称并不常见。 好吧,我同意反射的常见用途。但最初的问题是如何调用“GenericMethod()” 如果允许该语法,我们根本不需要 GetMethod()。但是对于“我如何编写”GenericMethod”这个问题?我认为答案应该包括一种避免丢失与 GenericMethod 的编译时链接的方法。现在这个问题是否常见我不知道,但是我确实知道我昨天遇到了这个确切的问题,这就是我提出这个问题的原因。 您可以使用GenMethod.Method.GetGenericMethodDefinition() 而不是this.GetType().GetMethod(GenMethod.Method.Name)。它稍微干净一些,可能更安全。 样本中的“myType”是什么意思? 现在可以使用nameof(GenericMethod)【参考方案3】:

使用dynamic 类型而不是反射 API 可以大大简化使用仅在运行时已知的类型参数调用泛型方法。

要使用这种技术,必须从实际对象(不仅仅是Type 类的实例)中知道类型。否则,您必须创建该类型的对象或使用标准反射 API solution。您可以使用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 中的caching 机制,下一次使用相同类型参数的调用应该会更快。当然,您可以编写缓存调用的委托的代码,但是通过使用 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 参数传递给方法时,此调用为lately bound。方法重载解析发生在运行时并尝试选择最佳重载。因此,如果您使用BarItem 类型的对象调用ProcessItem 方法,那么您实际上将调用非泛型方法,因为它更适合这种类型。但是,当您传递 Alpha 类型的参数时会出现运行时错误,因为没有可以处理此对象的方法(泛型方法具有约束 where T : IItemAlpha 类不实现此接口)。但这就是重点。编译器没有此调用有效的信息。作为程序员的你知道这一点,你应该确保这段代码运行没有错误。

返回类型陷阱

当您使用动态类型的参数调用非 void 方法时,其返回类型可能为 be dynamic too。因此,如果您将之前的示例更改为此代码:

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

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

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

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

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

【讨论】:

Mariusz,被“但是,当你传递 Alpha 类型的参数时会出现运行时错误,因为没有方法可以处理这个对象。”如果我调用 var a = new Alpha() ProcessItem (a,"test" + i, i) 为什么通用 ProcessItem 方法不能有效地处理这个问题,输出“General Process Item”? @AlexEdelstein 我编辑了我的答案以澄清一点。这是因为泛型ProcessItem 方法具有泛型约束并且只接受实现IItem 接口的对象。当您调用 ProcessItem(new Aplha(), "test" , 1);ProcessItem((object)(new Aplha()), "test" , 1); 时,您会收到编译器错误,但在转换为 dynamic 时,您会将检查推迟到运行时。 很好的答案和解释,非常适合我。比公认的答案好得多,写起来更短,性能更高,更安全。【参考方案4】:

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

【讨论】:

【参考方案5】:

添加到Adrian Gallero's answer:

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

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

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

其中GenericMethod&lt;object&gt; 是要调用的方法名称和任何满足通用约束的类型。

(Action) 匹配要调用的方法的签名,即(Func&lt;string,string,int&gt;Action&lt;bool&gt;

##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&lt;object&gt;) 指代特定方法的委托。 ((Func&lt;string, int&gt;)GenericMethod&lt;object&gt;) 指的是不同的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);

【讨论】:

【参考方案6】:

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

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&lt;String, int&gt;

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

【讨论】:

【参考方案7】:

这是基于Grax's answer 的我的 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 来找到你的 Generic 函数:

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。

【讨论】:

MakeGenericMethod() 的参数具有 params 关键字,因此您无需创建数组;您也不需要创建实例来获取类型 - methodInfo.MakeGenericMethod(typeof(TCollection), typeof(TObject)) 就足够了。【参考方案8】:

灵感来自Enigmativity's answer - 假设您有两个(或更多)类,例如

public class Bar  
public class Square  

你想用BarSquare调用方法Foo&lt;T&gt;,声明为

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

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

方形 酒吧

【讨论】:

以上是关于如何使用反射来调用泛型方法?的主要内容,如果未能解决你的问题,请参考以下文章

在.NET中使用反射调用泛型方法[重复]

c#简易反射调用泛型方法

JAVA反射问题,一个方法类的参数能否通过设置成泛型或者啥来接收反射的CLASS。

泛型与反射

如何使用给定的 Type 对象调用泛型方法? [复制]

java筑基.泛型,反射,注解-利用注解加反射练习