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

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

当前回答

你需要使用反射来获取方法,然后用MakeGenericMethod提供类型参数来“构造”它:

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

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

如上所述,在使用动态的c# 4中,很多事情都变得更简单了——当然,前提是您可以使用类型推断。它在类型推断不可用的情况下不起作用,例如问题中的确切示例。

其他回答

受Enigmativity的答案启发-让我们假设你有两个(或更多)类,比如

public class Bar { }
public class Square { }

你想调用方法Foo<T> with Bar and Square,它被声明为

public class myClass
{
    public void Foo<T>(T item)
    {
        Console.WriteLine(typeof(T).Name);
    }
}

然后你可以实现一个Extension方法:

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

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

广场 酒吧

通过使用动态类型而不是反射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函数的返回值。

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

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

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

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

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