首页 文章

如何使用反射来调用泛型方法?

提问于
浏览
890

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

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

7 回答

  • 144

    使用C#4.0,不需要反射,因为DLR可以使用运行时类型调用它 . 由于使用DLR库是一种动态的痛苦(而不是为您生成代码的C#编译器),开源框架Dynamitey(.net标准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}));
    
  • 113

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

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

    如果你想调用泛型方法,那么"normal"用法会推断出它的类型,那么它只是将未知类型的对象转换为 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 方法时,编译器不会使用正确的类型参数调用't care about the type of this argument. The compiler generates code that at runtime checks the real types of passed arguments (by using reflection) and choose the best method to call. Here there is only this one generic method, so it' .

    在此示例中,输出与您编写的内容相同:

    foreach (var o in objects)
    {
        MethodInfo method = typeof(Service).GetMethod("Process");
        MethodInfo generic = method.MakeGenericMethod(o.GetType());
        generic.Invoke(service, new object[] { o });
    }
    

    具有动态类型的版本肯定更短且更容易编写 . 您也不必担心多次调用此函数的性能 . 由于DLR中的caching机制,具有相同类型参数的下一个调用应该更快 . 当然,您可以编写缓存调用的委托的代码,但是通过使用 dynamic 类型,您可以免费获得此行为 .

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

    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 类型来执行某些方法 . 只有第一个参数's type is postponed to runtime. You will get a compiler error if the name of the method you' re调用的验证不存在或者其他参数无效(参数数量错误或类型错误) .

    dynamic 参数传递给方法时,此调用为lately bound . 方法重载解析在运行时发生,并尝试选择最佳过载 . 因此,如果使用 BarItem 类型的对象调用 ProcessItem 方法,那么当传递 Alpha 类型的参数时,'ll actually call the non-generic method, because it is a better match for this type. However, you' ll会收到运行时错误,因为没有可以处理此对象的方法(泛型方法具有约束 where T : IItemAlpha 类完全没有 . 编译器没有这个调用有效的信息 . 作为程序员你知道这一点,你应该确保这个代码运行没有错误 .

    返回类型问题

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

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

    那么结果对象的类型将是 dynamic . 这是因为编译器并不总是知道将调用哪个方法 . 如果你知道函数调用的返回类型,那么你应该implicitly convert它到所需的类型,所以其余的代码是静态类型的:

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

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

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

  • 6

    Nobody provided the "classic Reflection" solution, so here is a complete code example:

    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 上指定的 .

    Here is a complete example 如何调用此方法实例化并使用 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
    
  • 0

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

    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 ,如果你重构,删除或用_455120做任何事情,这段代码将继续工作,或者至少在编译时中断(例如你删除 GenericMethod ) .

    执行相同操作的其他方法是创建一个新的包装类,并通过 Activator 创建它 . 我不知道是否有更好的方法 .

  • 968

    这是基于Grax's answer的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 });
    

    而且你已经完成了 . 有魅力!

    UPDATE:

    正如@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 .

  • 3

    添加到Adrian Gallero's answer

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

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

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

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

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

    您需要使用反射来启动方法,然后使用MakeGenericMethod提供类型参数"construct":

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

    对于静态方法,将 null 作为 Invoke 的第一个参数传递 . 那个's nothing to do with generic methods - it'只是正常的反映 .

    如上所述,当使用 dynamic 时,很多这样的事情比C#4更简单 - 当然,如果你可以使用类型推断 . 它没有't help in cases where type inference isn' t可用,例如问题中的确切示例 .

相关问题