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


1069

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

考虑下面的示例代码-内部Example()方法,什么是最简洁的方式来调用GenericMethod<T>()使用Type存储在myType变量?

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>()
    {
        //...
    }
}

7
我尝试了Jon的解决方案,直到我在类中公开通用方法后,该解决方案才起作用。我知道另一个Jon回答说您需要指定bindingflags,但这没有帮助。
naskew 2012年

12
您还需要BindingFlags.Instance而不仅仅是BindingFlags.NonPublic私有/内部方法。
拉尔斯·凯曼

2
这个问题的现代版本:stackoverflow.com/q/2433436/103167
Ben Voigt 2014年

@Peter Mortensen-仅供参考,我在“?”之前使用空格 将英语部分与非英语(C#)部分分开;恕我直言,删除空间使它看起来像吗?是代码的一部分。如果没有代码,我当然会同意删除空格,但是在这种情况下……
Bevan 2015年

Answers:


1137

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

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

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

如前所述,从C#4开始,使用dynamic-如果您可以使用类型推断,那么很多这样的过程就更简单了。在类型推断不可用的情况下(例如问题中的确切示例),它无济于事。


92
+1; 请注意,GetMethod()默认情况下仅考虑公共实例方法,因此您可能需要BindingFlags.Static和/或BindingFlags.NonPublic

20
标志的正确组合是BindingFlags.NonPublic | BindingFlags.Instance(和可选BindingFlags.Static)。
拉尔斯·凯曼

4
一个被弄虚作假的问题使人们想知道如何使用静态方法做到这一点-从技术上讲,这里的问题也是如此。调用静态方法时,generic.Invoke()的第一个参数应为null。只有在调用实例方法时才需要第一个参数。
克里斯·莫斯基尼

2
@ChrisMoschini:将其添加到答案中。
乔恩·斯基特

2
@gzou:我已经打听到了答案-但请注意,用于调用泛型方法的问题dynamic因为类型推断不可用没有帮助。(编译器无法使用任何参数来确定类型参数。)
Jon Skeet 2015年

170

只是原始答案的补充。尽管这将起作用:

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。我不知道是否有更好的方法。


5
在使用反射调用方法的情况下,通常方法名称本身是由另一个方法发现的。事先知道方法名称并不常见。
Bevan

13
好吧,我同意反射的一般用途。但是最初的问题是如何调用“ GenericMethod <myType>()”。如果允许该语法,则根本不需要GetMethod()。但是对于“我如何编写“ GenericMethod <myType>””这个问题,我认为答案应该包括避免丢失与GenericMethod的编译时链接的方法。现在,这个问题是否常见,我不知道,但是我知道昨天我这个确切的问题,这就是为什么我在这个问题上降落。
阿德里安Gallero

20
您可以GenMethod.Method.GetGenericMethodDefinition()代替this.GetType().GetMethod(GenMethod.Method.Name)。它稍微更清洁,也许更安全。
丹尼尔·卡西迪

您的样本中的“ myType”是什么意思?
开发人员

37
现在您可以使用nameof(GenericMethod)
dmigo

140

通过使用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类型,我们推迟提供类型参数,直到运行时。当Process使用dynamic参数调用该方法时,编译器将不在乎此参数的类型。编译器生成的代码在运行时检查传递的参数的真实类型(通过使用反射),并选择最佳的调用方法。这里只有一个通用方法,因此可以使用适当的类型参数来调用它。

在此示例中,输出与您编写的相同:

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参数传递给方法时,此调用将被绑定。方法重载解析在运行时发生,并尝试选择最佳重载。因此,如果您ProcessItem使用BarItem类型为对象的对象调用该方法,那么您实际上将调用非泛型方法,因为它与该类型更匹配。但是,传递Alpha类型的参数时会出现运行时错误,因为没有方法可以处理此对象(通用方法具有约束,where T : IItemAlpha类未实现此接口)。但这就是重点。编译器没有此调用有效的信息。作为程序员,您知道这一点,并且应确保该代码运行无误。

返回类型陷阱

当你调用动态类型的参数的非空方法,它的返回类型可能dynamic。因此,如果将先前的示例更改为以下代码:

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

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

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”?
Alex Edelstein

@AlexEdelstein我编辑了答案以澄清一点。这是因为通用ProcessItem方法具有通用约束,并且仅接受实现IItem接口的对象。当您调用ProcessItem(new Aplha(), "test" , 1);或时ProcessItem((object)(new Aplha()), "test" , 1);,会出现编译器错误,但强制转换到dynamic您时,将检查运行时间。
Mariusz Pawelski

很好的答案和解释,非常适合我。比接受的答案好得多,写的时间短,性能更高,更安全。
ygoe 2015年

17

在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}));

13

加上Adrian Gallero的答案

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

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

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

其中GenericMethod<object>要调用的方法名称以及满足一般约束的任何类型。

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

第1步获取通用方法定义的MethodInfo

方法1:将GetMethod()或GetMethods()与适当的类型或绑定标志一起使用。

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

方法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的另一个重载

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

步骤2调用MakeGenericMethod创建具有适当类型的泛型方法。

MethodInfo generic = method.MakeGenericMethod(myType);

步骤3是使用适当的参数调用该方法。

generic.Invoke(this, null);

8

没有人提供“ 经典的反射 ”解决方案,因此这里是完整的代码示例:

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

这是一个完整的示例,说明如何调用此方法以实例化和使用a 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

2

这是我根据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来找到您的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))就足够了。
Bevan 2015年

0

谜团答案的启发-假设您有两个(或更多)类,例如

public class Bar { }
public class Square { }

并且您想Foo<T>使用Bar和调用方法Square,该方法声明为

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

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


酒吧

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.