关于c#:如何使用反射来调用泛型方法?

How do I use reflection to call a generic method?

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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提供类型参数来"构造"该方法:

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

对于静态方法,将null作为第一个参数传递给Invoke。这与一般方法无关——它只是普通反射。

如前所述,如果您可以使用类型推断,那么使用dynamic的C 4会更简单。在类型推断不可用的情况下,例如问题中的确切示例,它没有帮助。


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

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

这也有点危险,因为您会丢失对GenericMethod的编译时检查。如果您稍后进行重构并重命名GenericMethod,此代码将不会注意到,并在运行时失败。此外,如果程序集有任何后处理(例如混淆或删除未使用的方法/类),则此代码也可能中断。

因此,如果您知道在编译时链接到的方法,并且这不是数百万次调用,因此开销无关紧要,我将把此代码更改为:

1
2
3
4
5
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方法创建对象。好的。

如果您想调用一个泛型方法,在"normal"用法中会推断出它的类型,那么它只需将未知类型的对象强制转换为dynamic。下面是一个例子:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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)"
        }
    }
}

下面是这个程序的输出:好的。

1
2
3
4
5
6
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方法时,编译器不关心此参数的类型。编译器生成的代码在运行时检查传递参数的实际类型(通过使用反射),并选择要调用的最佳方法。这里只有这一个泛型方法,所以使用适当的类型参数调用它。好的。

在本例中,输出与编写时相同:好的。

1
2
3
4
5
6
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类型,您可以免费获得这种行为。好的。

如果要调用的泛型方法没有参数化类型的参数(因此无法推断其类型参数),则可以将泛型方法的调用包装在助手方法中,如下例所示:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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如何尝试解析要调用的方法。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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类不实现此接口)。但这就是重点。编译器没有此调用有效的信息。作为一个程序员,您应该知道这一点,并且应该确保这个代码运行时没有错误。好的。返回类型gotcha

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

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

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

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

如果类型不匹配,您将得到一个运行时错误。好的。

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


对于C 4.0,反射是不必要的,因为DLR可以使用运行时类型来调用它。由于动态地使用DLR库(而不是为您生成代码的C编译器),因此开源框架动态(.NET标准1.5)为您提供了对编译器将为您生成的相同调用的轻松缓存运行时访问。

1
2
3
4
5
6
var name = InvokeMemberName.Create;
Dynamic.InvokeMemberAction(this, name("GenericMethod", new[]{myType}));


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

加上阿德里安·加莱罗的回答:

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

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

1
2
3
4
5
((Action)GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition()
    .MakeGenericMethod(typeof(string))
    .Invoke(this, null);

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

(action)匹配要调用的方法的签名,即(FuncAction

步骤1正在获取泛型方法定义的MethodInfo方法1:使用带有适当类型或绑定标志的getmethod()或getmethods()。

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

方法2:创建委托,获取MethodInfo对象,然后调用GetGenericMethodDefinition

从包含方法的类内部:

1
2
3
4
5
6
7
MethodInfo method = ((Action)GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition();

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

从包含方法的类外部:

1
2
3
4
5
6
7
8
MethodInfo method = ((Action)(new Sample())
    .GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition();

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

在C中,方法的名称,即"ToString"或"GenericMethod"实际上是指可能包含一个或多个方法的一组方法。在提供方法参数的类型之前,不知道您所指的方法。

((Action)GenericMethod)是指特定方法的委托。((Func)GenericMethod)引用泛型方法的另一个重载

方法3:创建包含方法调用表达式的lambda表达式,获取MethodInfo对象,然后获取GetGenericMethodDefinition

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

这可以归结为

创建一个lambda表达式,其中主体是对所需方法的调用。

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

提取主体并强制转换为方法calExpression

1
MethodCallExpression methodCallExpr = (MethodCallExpression)expr.Body;

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

1
MethodInfo methodA = methodCallExpr.Method.GetGenericMethodDefinition();

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

1
MethodInfo generic = method.MakeGenericMethod(myType);

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

1
generic.Invoke(this, null);

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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");
        }
    }
}

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

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


这是我的2美分,基于Grax的答案,但有两个通用方法所需的参数。

假设您的方法在助手类中定义如下:

1
2
3
4
5
6
7
8
public class Helpers
{
    public static U ConvertCsvDataToCollection<U, T>(string csvData)
    where U : ObservableCollection<T>
    {
      //transform code here
    }
}

在我的例子中,u类型始终是一个可观察的集合,存储t类型的对象。

由于我已经预先定义了类型,所以我首先创建了表示可观察集合(U)和存储在其中的对象(T)的"虚拟"对象,这些对象将在下面用于在调用make时获取其类型。

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

然后调用getmethod查找泛型函数:

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

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

需要将类型[]数组传递给包含上面创建的"dummy"对象类型的MakeGenericMethod函数:

1
2
3
4
5
MethodInfo generic = method.MakeGenericMethod(
new Type[] {
   myCollection.GetType(),
   myObject.GetType()
});

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

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

你完成了。作品魅力无穷!

更新:

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

1
2
3
4
5
6
7
8
9
10
11
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。