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

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

当前回答

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

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

广场 酒吧

其他回答

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

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

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。