如何使用 Type 变量调用泛型方法?

How do I call a generic method using a Type variable?

提问人:Bevan 提问时间:10/24/2008 最后编辑:gunr2171Bevan 更新时间:9/16/2023 访问量:355752

问:

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

考虑以下示例代码 - 在方法中,使用存储在变量中的调用的最简洁方法是什么?Example()GenericMethod<T>()TypemyType

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# .NET 泛型 反射

评论

9赞 naskew 6/14/2012
我尝试了 Jon 的解决方案,直到我在类中公开泛型方法后才能让它工作。我知道另一个 Jon 回复说您需要指定绑定标志,但这没有帮助。
14赞 Lars Kemmann 2/16/2013
您还需要 ,而不仅仅是 ,来获取私有/内部方法。BindingFlags.InstanceBindingFlags.NonPublic
2赞 Ben Voigt 9/13/2014
这个问题的现代版本: stackoverflow.com/q/2433436/103167
0赞 Bevan 11/29/2015
@Peter Mortensen - 仅供参考,我在“?”之前使用了空格来分隔英语部分和非英语 (C#) 部分;恕我直言,删除空格使它看起来像?是代码的一部分。如果没有代码,我当然会同意删除空格,但在这种情况下......

答:

1345赞 Jon Skeet 10/24/2008 #1

您需要使用反射来获取要开始的方法,然后通过使用 MakeGenericMethod 提供类型参数来“构造”它:

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

对于静态方法,将第一个参数传递给 。这与通用方法无关 - 它只是正常的反射。nullInvoke

如前所述,从 C# 4 开始,其中很多都更简单了 - 当然,如果你可以使用类型推断的话。在类型推断不可用的情况下,它无济于事,例如问题中的确切示例。dynamic

评论

118赞 12/17/2011
+1;请注意,默认情况下仅考虑公共实例方法,因此您可能需要 和/或 .GetMethod()BindingFlags.StaticBindingFlags.NonPublic
29赞 Lars Kemmann 2/16/2013
标志的正确组合是 (和可选的 )。BindingFlags.NonPublic | BindingFlags.InstanceBindingFlags.Static
5赞 Chris Moschini 3/23/2013
一个被标记为欺骗的问题想知道如何使用静态方法做到这一点 - 从技术上讲,这里的问题也是如此。通用。调用静态方法时,Invoke() 的第一个参数应为 null。第一个参数仅在调用实例方法时才需要。
2赞 Jon Skeet 3/23/2013
@ChrisMoschini:将其添加到答案中。
3赞 Jon Skeet 4/24/2015
@gzou:我在答案中添加了一些东西 - 但请注意,对于调用问题中的泛型方法,没有帮助,因为类型推断不可用。(编译器没有可用于确定类型参数的参数。dynamic
192赞 Adrian Gallero 2/28/2011 #2

只是对原始答案的补充。虽然这将起作用:

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

这也有点危险,因为您丢失了 的编译时检查。如果稍后执行重构并重命名,则此代码不会注意到,并且在运行时会失败。此外,如果对程序集进行任何后处理(例如,模糊处理或删除未使用的方法/类),则此代码也可能会中断。GenericMethodGenericMethod

因此,如果您知道在编译时链接到的方法,并且该方法没有被调用数百万次,因此开销无关紧要,我会将此代码更改为:

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

虽然不是很漂亮,但你有一个编译时引用,如果你重构、删除或做任何事情,这段代码将继续工作,或者至少在编译时中断(例如,如果你删除了)。GenericMethodGenericMethodGenericMethod

执行相同操作的另一种方法是创建一个新的包装类,并通过 .我不知道有没有更好的方法。Activator

评论

6赞 Bevan 2/28/2011
在使用反射来调用方法的情况下,方法名称本身通常由另一个方法发现。提前知道方法名称并不常见。
16赞 Adrian Gallero 3/1/2011
好吧,我同意反射的常见用途。但最初的问题是如何调用“GenericMethod<myType>()” 如果允许该语法,我们根本不需要 GetMethod()。但是对于“我如何编写”GenericMethod<myType>“的问题?我认为答案应该包括一种避免丢失与 GenericMethod 的编译时链接的方法。现在这个问题是否常见我不知道,但我知道我昨天遇到了这个问题,这就是我遇到这个问题的原因。
20赞 Daniel Cassidy 5/10/2011
你可以代替 .它稍微干净一些,可能更安全。GenMethod.Method.GetGenericMethodDefinition()this.GetType().GetMethod(GenMethod.Method.Name)
42赞 dmigo 3/17/2016
现在你可以使用nameof(GenericMethod)
1赞 Ben Voigt 7/15/2022
@EricScherrer:不应该ActionAction<>
19赞 jbtule 7/5/2011 #3

在 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}));
180赞 Mariusz Pawelski 3/17/2014 #4

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

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

如果要调用泛型方法,则在“正常”用法中会推断出其类型,则只需将未知类型的对象转换为 。下面是一个示例: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

通过将对象参数转换为类型,我们将提供类型参数推迟到运行时。当使用参数调用该方法时,编译器不关心此参数的类型。编译器生成的代码在运行时检查传递参数的实际类型(通过使用反射),并选择要调用的最佳方法。这里只有一个泛型方法,因此使用适当的类型参数调用它。dynamicProcessdynamic

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

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

如果要调用的泛型方法没有参数化类型的参数(因此无法推断其类型参数),则可以将泛型方法的调用包装在帮助程序方法中,如以下示例所示:

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

提高类型安全性

使用 object 来替代使用反射 API 的真正好处在于,您只会丢失这种特定类型的编译时检查,而这种检查直到运行时才知道。编译器像往常一样静态分析其他参数和方法名称。如果删除或添加更多参数、更改其类型或重命名方法名称,则会出现编译时错误。如果将方法名称作为字符串提供,并将参数作为对象数组提供。dynamicType.GetMethodMethodInfo.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

当您将参数传递给方法时,此调用是最近绑定的。方法重载解析发生在运行时,并尝试选择最佳重载。因此,如果使用类型的对象调用该方法,则实际上将调用非泛型方法,因为它更适合此类型。但是,当您传递该类型的参数时,您将收到运行时错误,因为没有可以处理此对象的方法(泛型方法具有约束,并且类不实现此接口)。但这就是重点。编译器没有此调用有效的信息。作为程序员,您知道这一点,并且应该确保此代码运行没有错误。dynamicProcessItemBarItemAlphawhere T : IItemAlpha

返回类型陷阱

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

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

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

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

如果类型不匹配,则会出现运行时错误。

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

评论

0赞 Alex Edelstein 3/22/2015
Mariusz,对“但是,当您传递 Alpha 类型的参数时,您会收到运行时错误,因为没有方法可以处理此对象。如果我调用 var a = new Alpha() ProcessItem(a,“test” + i, i) 为什么通用的 ProcessItem 方法不能有效地处理这个问题,输出“一般流程项”?
1赞 Mariusz Pawelski 3/23/2015
@AlexEdelstein我编辑了我的答案以澄清一点。这是因为泛型方法具有泛型约束,并且只接受实现接口的对象。当您调用或收到编译器错误时,但在强制转换为时,请将该检查推迟到运行时。ProcessItemIItemProcessItem(new Aplha(), "test" , 1);ProcessItem((object)(new Aplha()), "test" , 1);dynamic
0赞 ygoe 8/28/2015
很好的答案和解释,对我来说非常有效。比公认的答案好得多,编写时间更短,性能更高,更安全。
23赞 Grax32 1/10/2015 #5

补充Adrian Gallero的回答

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

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

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

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

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

##Step 1 正在获取泛型方法定义的 MethodInfo##

###Method 1:使用带有适当类型或绑定标志的 GetMethod() 或 GetMethods()。###

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");

###Method 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>)引用特定方法的委托。 指 GenericMethod 的不同重载((Func<string, int>)GenericMethod<object>)

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

##Step 2 正在调用 MakeGenericMethod 以创建具有适当类型的泛型方法。##

MethodInfo generic = method.MakeGenericMethod(myType);

##Step 3 使用适当的参数调用该方法。##

generic.Invoke(this, null);
4赞 Thierry 10/23/2015 #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 函数时,我不需要创建数组,因为它接受参数,并且我不需要创建对象来获取类型,因为我可以将类型直接传递给此函数。就我而言,由于我在另一个类中预定义了类型,因此我只是将代码更改为:

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 中使用这些类型。Type

再次感谢您强调此@Bevan。

评论

0赞 Bevan 10/27/2015
具有 params 关键字的参数,因此您不需要创建数组;您也不需要创建实例来获取类型 - 就足够了。MakeGenericMethod()methodInfo.MakeGenericMethod(typeof(TCollection), typeof(TObject))
12赞 Dimitre Novatchev 8/24/2016 #7

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

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
0赞 Matt 3/3/2020 #8

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

public class Bar { }
public class Square { }

并且您想使用 和 调用方法,该方法声明为Foo<T>BarSquare

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

适用于每个班级。在这种情况下,它将输出:

方形
酒吧

0赞 Evgeny Shmanev 7/24/2023 #9

尽管这是一个相当古老的问题,但我发现它很有趣,因为有几个选项可以动态调用方法。从字面上看,它是反射、表达式树和发射器。从历史上看,反射是最慢的选择,而发射器是最快的选择。因此,我决定在这个有趣的案例中比较它们,看看现在是否有任何变化。最初的问题要求**在编译时不知道类型参数时调用泛型方法的最佳方式**。然而,上面几乎所有的答案都建议使用反射。

我为所有提到的方法创建了三个测试用例。首先,下面是一个稍作修改的示例类,它将使用 3 种方法进行测试:TestReflection、TestExpression 和 TestEmit。

public class Sample
{
    public void TestDirectCall(Type type)
    {
        GenericMethod<string>();
        GenericMethodWithArg<string>(42);
        StaticMethod<string>();
        StaticMethodWithArg<string>(6);
    }

    public void TestReflection(Type type)
    {
        CallViaReflection.CallGenericMethod(this, type);
        CallViaReflection.CallGenericMethod(this, type, 42);
        CallViaReflection.CallStaticMethod(type);
        CallViaReflection.CallStaticMethod(type, 6);
    }

    public void TestExpression(Type type)
    {
        CallViaExpression.CallGenericMethod(this, type);
        CallViaExpression.CallGenericMethod(this, type, 42);
        CallViaExpression.CallStaticMethod(type);
        CallViaExpression.CallStaticMethod(type, 6);
    }

    public void TestEmit(Type type)
    {
        CallViaEmit.CallGenericMethod(this, type);
        CallViaEmit.CallGenericMethod(this, type, 42);
        CallViaEmit.CallStaticMethod(type);
        CallViaEmit.CallStaticMethod(type, 6);
    }

    public void T()
    {
        StaticMethod<string>();
    }

    public void GenericMethod<T>()
    {
    }

    public void GenericMethodWithArg<T>(int someValue)
    {
    }

    public static void StaticMethod<T>()
    {
    }

    public static void StaticMethodWithArg<T>(int someValue)
    {
    }
}

类 CallViaReflection 表示一个帮助程序类,该类通过反射调用泛型方法。我决定引入缓存以获得更好的结果。

public static class CallViaReflection
{
    private readonly static Cache<MethodInfo> cache = new();

    public static void CallGenericMethod(Sample sample, Type genericType)
    {
        var callDelegate = GetDelegate(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
        callDelegate.Invoke(sample, null);
    }

    public static void CallGenericMethod(Sample sample, Type genericType, int someValue)
    {
        var callDelegate = GetDelegate(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType, typeof(int));
        callDelegate.Invoke(sample, new object[] { someValue });
    }

    public static void CallStaticMethod(Type genericType)
    {
        var callDelegate = GetDelegate(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
        callDelegate.Invoke(null, null);
    }

    public static void CallStaticMethod(Type genericType, int someValue)
    {
        var callDelegate = GetDelegate(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType, typeof(int));
        callDelegate.Invoke(null, new object[] { someValue });
    }

    private static MethodInfo GetDelegate(string methodName, BindingFlags bindingFlags, Type genericType, params Type[] arguments)
    {
        if (cache.TryGet(methodName, genericType, out var concreteMethodInfo))
            return concreteMethodInfo;

        var sampleType = typeof(Sample);
        MethodInfo genericMethodInfo = sampleType.GetMethod(methodName, bindingFlags)!;
        concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
        cache.Add(methodName, genericType, concreteMethodInfo);
        return concreteMethodInfo;
    }
}

下一个类 CallViaExpression 使用缓存的表达式树。

public static class CallViaExpression
{
    private static readonly Cache<Delegate> cache = new();

    public static void CallGenericMethod(Sample sample, Type genericType)
    {
        var callDelegate = GetDelegate(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
        ((Action<Sample>)callDelegate).Invoke(sample);
    }

    public static void CallGenericMethod(Sample sample, Type genericType, int someValue)
    {
        var callDelegate = GetDelegate(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType, typeof(int));
        ((Action<Sample, int>)callDelegate).Invoke(sample, someValue);
    }

    public static void CallStaticMethod(Type genericType)
    {
        var callDelegate = GetDelegate(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
        ((Action)callDelegate).Invoke();
    }

    public static void CallStaticMethod(Type genericType, int someValue)
    {
        var callDelegate = GetDelegate(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType, typeof(int));
        ((Action<int>)callDelegate).Invoke(someValue);
    }

    private static Delegate GetDelegate(string methodName, BindingFlags bindingFlags, Type genericType, params Type[] arguments)
    {
        if (cache.TryGet(methodName, genericType, out var callDelegate))
            return callDelegate;

        var sampleType = typeof(Sample);
        MethodInfo genericMethodInfo = sampleType.GetMethod(methodName, bindingFlags)!;
        var concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);

        var argumentExpr = arguments.Select((type, i) => Expression.Parameter(type, "arg" + i)).ToArray();
        if (concreteMethodInfo.IsStatic)
        {
            var callExpr = Expression.Call(concreteMethodInfo, argumentExpr);
            callDelegate = Expression.Lambda(callExpr, argumentExpr).Compile();
        }
        else
        {
            var parameterExpr = Expression.Parameter(sampleType, "sample");
            var callExpr = Expression.Call(parameterExpr, concreteMethodInfo, argumentExpr);
            callDelegate = Expression.Lambda(callExpr, new[] { parameterExpr }.Union(argumentExpr).ToArray()).Compile();
        }

        cache.Add(methodName, genericType, callDelegate);
        return callDelegate;
    }
}

最后但并非最不重要的一点是,CallViaEmit 会发出必要的操作。

public static class CallViaEmit
{
    private static readonly Cache<Delegate> cache = new();

    public static void CallGenericMethod(this Sample sample, Type genericType)
    {
        var callDelegate = GetDynamicMethod(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
        ((Action<Sample>)callDelegate).Invoke(sample);
    }

    public static void CallGenericMethod(this Sample sample, Type genericType, int someValue)
    {
        var callDelegate = GetDynamicMethod(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType);
        ((Action<Sample, int>)callDelegate).Invoke(sample, someValue);
    }

    public static void CallStaticMethod(Type genericType)
    {
        var callDelegate = GetDynamicMethod(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
        ((Action)callDelegate).Invoke();
    }

    public static void CallStaticMethod(Type genericType, int someValue)
    {
        var callDelegate = GetDynamicMethod(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType);
        ((Action<int>)callDelegate).Invoke(someValue);
    }

    private static Delegate GetDynamicMethod(string methodName, BindingFlags bindingFlags, Type genericType)
    {
        if (cache.TryGet(methodName, genericType, out var callDelegate))
            return callDelegate;

        var genericMethodInfo = typeof(Sample).GetMethod(methodName, bindingFlags)!;
        var concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
        var argumentTypes = concreteMethodInfo.GetParameters().Select(x => x.ParameterType).ToArray(); ;
        var dynamicMethodArgs = concreteMethodInfo.IsStatic
            ? argumentTypes
            : new[] { typeof(Sample) }.Union(argumentTypes).ToArray();

        var dynamicMethod = new DynamicMethod("DynamicCall", null, dynamicMethodArgs);
        var il = dynamicMethod.GetILGenerator();
        il.Emit(OpCodes.Nop);

        switch (dynamicMethodArgs.Length)
        {
            case 0:
                break;
            case 1:
                il.Emit(OpCodes.Ldarg_0);
                break;
            case 2:
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                break;
            case 3:
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Ldarg_2);
                break;
            default:
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Ldarg_2);
                il.Emit(OpCodes.Ldarg_3);
                for (int i = 4; i < argumentTypes.Length; i++)
                {
                    il.Emit(OpCodes.Ldarg, argumentTypes[i]);
                }
                break;
        }

        il.EmitCall(concreteMethodInfo.IsStatic ? OpCodes.Call : OpCodes.Callvirt, concreteMethodInfo, null);
        il.Emit(OpCodes.Nop);
        il.Emit(OpCodes.Ret);

        callDelegate = dynamicMethod.CreateDelegate(GetActionType(dynamicMethodArgs));
        cache.Add(methodName, genericType, callDelegate);
        return callDelegate;
    }

    private static Type GetActionType(Type[] argumentTypes)
    {
        switch (argumentTypes.Length)
        {
            case 0:
                return typeof(Action);
            case 1:
                return typeof(Action<>).MakeGenericType(argumentTypes);
            case 2:
                return typeof(Action<,>).MakeGenericType(argumentTypes);
            case 3:
                return typeof(Action<,,>).MakeGenericType(argumentTypes);
            case 4:
                return typeof(Action<,,,>).MakeGenericType(argumentTypes);
            case 5:
                return typeof(Action<,,,,>).MakeGenericType(argumentTypes);
            case 6:
                return typeof(Action<,,,,,>).MakeGenericType(argumentTypes);
            case 7:
                return typeof(Action<,,,,,,>).MakeGenericType(argumentTypes);
            case 8:
                return typeof(Action<,,,,,,,>).MakeGenericType(argumentTypes);
            default:
                throw new NotSupportedException("Action with more than 8 arguments is not supported");
        }
    }
}

最后,这里是一个测试类和测试结果。

[TestFixture]
public class SampleTests
{
    private const int Iterations = 10000000;

    [Test]
    public void TestDirectCall()
    {
        var sample = new Sample();
        var stopwatch = new Stopwatch();
        stopwatch.Start();

        for (var i = 0; i < Iterations; i++)
            sample.TestDirectCall(typeof(string));

        stopwatch.Stop();
        Assert.Pass($"Calling methods directly took {stopwatch.ElapsedMilliseconds} milliseconds.");
    }

    [Test]
    public void TestReflection()
    {
        var sample = new Sample();
        var stopwatch = new Stopwatch();
        stopwatch.Start();

        for (var i = 0; i < Iterations; i++)
            sample.TestReflection(typeof(string));

        stopwatch.Stop();
        Assert.Pass($"Calling methods dynamically via reflection took {stopwatch.ElapsedMilliseconds} milliseconds.");
    }

    [Test]
    public void TestExpressionTree()
    {
        var sample = new Sample();
        var stopwatch = new Stopwatch();
        stopwatch.Start();

        for (var i = 0; i < Iterations; i++)
            sample.TestExpression(typeof(string));

        stopwatch.Stop();
        Assert.Pass($"Calling methods dynamically via expression tree took {stopwatch.ElapsedMilliseconds} milliseconds.");
    }

    [Test]
    public void TestEmit()
    {
        var sample = new Sample();
        var stopwatch = new Stopwatch();
        stopwatch.Start();

        for (var i = 0; i < Iterations; i++)
            sample.TestEmit(typeof(string));

        stopwatch.Stop();
        Assert.Pass($"Calling methods dynamically via emit took {stopwatch.ElapsedMilliseconds} milliseconds.");
    }
}

通过 emit 动态调用方法需要 2939 毫秒。 通过表达式树动态调用方法需要 3910 毫秒。 通过反射动态调用方法需要 6381 毫秒。

显然,赢家是发射极。它的性能仍然快两倍以上。排在第二位的是表达式树。

因此,我的判断在第二个十年中还没有改变。如果需要动态调用方法,请开始使用表达式树。如果代码对性能至关重要,请使用 ILGenerator 并发出必要的调用。尽管如此,乍一看它可能看起来很复杂,所有必要的 MSIL 指令都可以使用 ildasm 轻松拆解。

源代码可在 GitHub 上找到。

评论

0赞 Luca Cremonesi 9/16/2023
如果你的代码不是性能关键的,那么我建议你使用最易读的选项,那就是反射。
0赞 Evgeny Shmanev 9/17/2023
@LucaCremonesi这是关于习惯的。当我习惯使用表达式树时,我就停止使用反射了。当你理解表达式树时,它们很容易,但老实说,学习曲线是很难的