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

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

当前回答

在c# 4.0中,反射是不必要的,因为DLR可以使用运行时类型调用它。由于动态地使用DLR库有点麻烦(而不是c#编译器为您生成代码),开源框架Dynamitey (.net standard 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}));

其他回答

通过使用动态类型而不是反射API,可以极大地简化使用只有在运行时才知道的类型参数来调用泛型方法。

要使用这种技术,必须从实际对象(而不仅仅是type类的实例)中了解类型。否则,您必须创建该类型的对象或使用标准反射API解决方案。您可以使用Activator创建对象。CreateInstance除外的方法。

如果你想调用一个泛型方法,在“正常”用法中,它会推断出它的类型,那么它只是将未知类型的对象强制转换为动态的。这里有一个例子:

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操作符)。

通过将对象参数转换为动态类型,我们将类型参数的提供推迟到运行时。当使用动态参数调用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中的缓存机制,下一次具有相同类型参数的调用应该更快。当然,您可以编写代码缓存调用的委托,但通过使用动态类型,您可以免费获得这种行为。

如果你想调用的泛型方法没有参数化类型的参数(所以它的类型参数不能被推断出来),那么你可以将泛型方法的调用包装在一个助手方法中,如下例所示:

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) + ">");
    }
}

增加型号安全性

What is really great about using dynamic object as a replacement for using reflection API is that you only lose compile time checking of this particular type that you don't know until runtime. Other arguments and the name of the method are staticly analysed by the compiler as usual. If you remove or add more arguments, change their types or rename method name then you'll get a compile-time error. This won't happen if you provide the method name as a string in Type.GetMethod and arguments as the objects array in 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);
    }
}

在这里,我们再次通过将参数强制转换为动态类型来执行一些方法。只有第一个参数类型的验证被推迟到运行时。如果所调用的方法的名称不存在或其他参数无效(参数数量错误或类型错误),则会得到编译器错误。

When you pass the dynamic argument to a method then this call is lately bound. Method overload resolution happens at runtime and tries to choose the best overload. So if you invoke the ProcessItem method with an object of BarItem type then you'll actually call the non-generic method, because it is a better match for this type. However, you'll get a runtime error when you pass an argument of the Alpha type because there's no method that can handle this object (a generic method has the constraint where T : IItem and Alpha class doesn't implement this interface). But that's the whole point. The compiler doesn't have information that this call is valid. You as a programmer know this, and you should make sure that this code runs without errors.

返回类型gotcha

当你用动态类型的参数调用一个非void方法时,它的返回类型也可能是动态的。所以如果你把前面的例子改为下面的代码:

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

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

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

如果类型不匹配,则会得到一个运行时错误。

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

在阿德里安·加勒罗的回答中补充道:

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

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

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

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

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

第一步是获取泛型方法定义的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 <字符串,int >) GenericMethod <对象>) 引用GenericMethod的不同重载

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

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

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

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(类型keyType,类型valueType)

并且它创建并返回一个字典实例,其键和值的类型恰好是调用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找到你的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个类型类型的属性,我在运行时根据传递给构造函数的枚举值设置,并将为我提供相关类型,然后在MakeGenericMethod中使用。

再次感谢你强调这个@Bevan。

只是对原来答案的一个补充。虽然这是可行的:

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