提问



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


考虑以下示例代码 - 在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提供类型参数来构造它:[78]


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


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


如上所述,使用dynamic的C#4中的很多内容更简单 - 当然,如果你可以使用类型推断。在类型推断不可用的情况下,它没有帮助,例如问题中的确切示例。

其它参考1


只是对原始答案的补充。虽然这会奏效:


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

其它参考2


通过使用dynamic类型而不是反射API,可以大大简化使用仅在运行时知道的类型参数调用泛型方法。[79]


要使用此技术,必须从实际对象(不仅仅是Type类的实例)中获知类型。否则,您必须创建该类型的对象或使用标准反射API解决方案。您可以使用Activator.CreateInstance方法创建对象。 [81]


如果你想调用泛型方法,那么在正常用法中会推断出它的类型,那么它只是将未知类型的对象转换为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运算符)。


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


如果您要调用的泛型方法没有参数化类型的参数(因此无法推断其类型参数),那么您可以将循环方法的调用包装在辅助方法中,如下例所示:


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


返回类型问题



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


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


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


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


如果类型不匹配,您将收到运行时错误。


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

其它参考3


使用C#4.0,反射是不必要的,因为DLR可以使用运行时类型调用它。由于使用DLR库是一种动态的痛苦(而不是C#编译器为您生成代码),开源框架Dynamitey(.net)标准1.5)为您提供了对编译器为您生成的相同调用的简单缓存运行时访问。[86]


var name = InvokeMemberName.Create;
Dynamic.InvokeMemberAction(this, name("GenericMethod", new[]{myType}));


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

其它参考4


加上Adrian Gallero的回答:


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


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



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


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


(动作)匹配要调用的方法的签名,即(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);

其它参考5


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


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

其它参考6


根据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函数时创建一个数组,因为它接受params并且我不需要创建一个对象来获取类型,因为我可以直接将类型传递给这个函数。在我的情况下,由于我在另一个类中预定义了类型,我只是将我的代码更改为:


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。