当类型参数在编译时未知,而是在运行时动态获取时,调用泛型方法的最佳方法是什么?
考虑以下示例代码-在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>()
{
//...
}
}
字符串
9条答案
按热度按时间j0pj023g1#
首先,你需要使用反射来获取方法,然后通过提供MakeGenericMethod类型参数来“构造”它:
字符串
对于静态方法,将
null
作为第一个参数传递给Invoke
。这与泛型方法无关--它只是正常的反射。如上所述,在C# 4中使用
dynamic
,很多操作都比较简单--当然,如果你可以使用类型推断的话。在类型推断不可用的情况下,它没有帮助,例如问题中的确切例子。brccelvz2#
只是对原始答案的补充。虽然这将工作:
字符串
这也有点危险,因为您丢失了
GenericMethod
的编译时检查。如果您稍后执行重构并重命名GenericMethod
,则代码不会注意到,并且会在运行时失败。此外,如果对程序集进行了任何后期处理(例如模糊处理或删除未使用的方法/类),则代码也可能会中断。所以,如果你知道你在编译时链接到的方法,并且这个方法没有被调用数百万次,所以开销并不重要,我会把这段代码改为:
型
虽然不是很漂亮,但这里有一个编译时引用
GenericMethod
,如果你重构,删除或对GenericMethod
做任何事情,这段代码将继续工作,或者至少在编译时中断(例如,如果你删除GenericMethod
)。另一种方法是创建一个新的 Package 类,并通过
Activator
创建它。我不知道有没有更好的办法。wh6knrhe3#
通过使用
dynamic
类型而不是反射API,可以极大地简化使用只有在运行时才知道的类型参数调用泛型方法的过程。要使用这种技术,必须从实际对象(而不仅仅是
Type
类的示例)中知道类型。否则,您必须创建该类型的对象或使用标准反射API solution。可以使用Activator.CreateInstance方法创建对象。如果你想调用一个泛型方法,在“正常”使用中,它的类型会被推断出来,那么它只需要将未知类型的对象强制转换为
dynamic
。下面是一个示例:字符串
下面是这个程序的输出:
型
Process
是一个泛型示例方法,它写入传递参数的真实的类型(通过使用GetType()
方法)和泛型参数的类型(通过使用typeof
运算符)。通过将对象参数强制转换为
dynamic
类型,我们将提供类型参数的时间推迟到运行时。当使用dynamic
参数调用Process
方法时,编译器不关心该参数的类型。编译器生成的代码在运行时检查传递的参数的真实的类型(通过使用反射),并选择最佳方法进行调用。这里只有一个泛型方法,所以它是用一个合适的类型参数调用的。在本例中,输出与您编写的输出相同:
型
动态类型的版本肯定更短,更容易编写。您也不必担心多次调用此函数的性能。由于DLR中的caching机制,下一次使用相同类型参数的调用应该会更快。当然,您可以编写缓存调用委托的代码,但是通过使用
dynamic
类型,您可以免费获得这种行为。如果你想要调用的泛型方法没有参数化类型的参数(因此它的类型参数不能被推断),那么你可以像下面的例子一样在助手方法中 Package 泛型方法的调用:
型
增加类型安全性
使用
dynamic
对象作为反射API的替代品的真正好处是,您只会丢失对这种特定类型的编译时检查,直到运行时才知道。其他参数和方法名由编译器像往常一样静态分析。如果您删除或添加更多参数,更改其类型或重命名方法名称,那么您将获得编译时错误。如果在Type.GetMethod
中以字符串的形式提供方法名,在MethodInfo.Invoke
中以对象数组的形式提供参数,则不会发生这种情况。下面是一个简单的例子,说明了如何在编译时捕获一些错误(注解代码),以及如何在运行时捕获其他错误。它还展示了DLR如何尝试解析要调用的方法。
型
这里我们再次通过将参数转换为
dynamic
类型来执行某个方法。只有第一个参数的类型验证被推迟到运行时。如果你调用的方法名不存在或者其他参数无效(参数数量错误或者类型错误),你会得到一个编译器错误。当你把
dynamic
参数传递给一个方法时,这个调用就是lately bound。方法重载解析发生在运行时,并尝试选择最佳重载。因此,如果您使用BarItem
类型的对象调用ProcessItem
方法,则实际上将调用非泛型方法,因为它更适合此类型。但是,当您传递Alpha
类型的参数时,您会得到一个运行时错误,因为没有可以处理此对象的方法(泛型方法具有where T : IItem
约束,而Alpha
类没有实现此接口)。但这才是重点。编译器没有此调用有效的信息。作为一名程序员,您应该知道这一点,并且应该确保此代码运行时没有错误。返回类型gotcha
当你用动态类型的参数调用一个非void方法时,它的返回类型可能是be
dynamic
too。所以如果你把前面的例子改成下面的代码:型
则结果对象的类型将是
dynamic
。这是因为编译器并不总是知道将调用哪个方法。如果你知道函数调用的返回类型,那么你应该将它implicitly convert为所需的类型,这样剩下的代码都是静态类型的:型
如果类型不匹配,您将得到一个运行时错误。
实际上,如果您试图在前面的示例中获取结果值,那么您将在第二次循环迭代中获得运行时错误。这是因为您试图保存void函数的返回值。
tquggr8v4#
Adrian Gallero的回答:
从类型信息调用泛型方法涉及三个步骤。
TLDR:使用类型对象调用已知的泛型方法可以通过以下方式实现:##
字符串
其中
GenericMethod<object>
是要调用的方法名和任何满足泛型约束的类型。(Action)匹配要调用的方法的签名,即(
Func<string,string,int>
或Action<bool>
)第一步获取泛型方法定义的MethodInfo
方法一:使用具有适当类型或绑定标志的GetMethod()或GetMethods()。#
型
方法二:创建委托,获取MethodInfo对象,然后调用GetGenericMethodDefinition
从包含方法的类内部:
型
从包含方法的类外部:
型
在C#中,方法的名称,即“ToString”或“GenericMethod”实际上是指一组可能包含一个或多个方法的方法。在您提供方法参数的类型之前,不知道您引用的是哪个方法。
((Action)GenericMethod<object>)
引用特定方法的委托。((Func<string, int>)GenericMethod<object>)
引用GenericMethod的另一个重载方法三:创建一个包含方法调用表达式的lambda表达式,获取MethodInfo对象,然后获取GetGenericMethodDefinition
型
这可以分解为
创建一个lambda表达式,其中主体是对所需方法的调用。
型
提取主体并转换为MethodCallExpression
型
从方法中获取泛型方法定义
型
第二步调用MakeGenericMethod创建一个类型合适的泛型方法。##
型
第三步是使用合适的参数调用方法。##
型
htzpubme5#
在C# 4.0中,反射不是必需的,因为DLR可以使用运行时类型调用它。由于动态地使用DLR库是一种痛苦(而不是C#编译器为您生成代码),开源框架Dynamitey(.net标准1.5)为您提供了对编译器将为您生成的相同调用的轻松缓存运行时访问。
字符串
2guxujil6#
没有人提供“classic Reflection”的解决方案,所以这里有一个完整的代码示例:
字符串
上面的
DynamicDictionaryFactory
类有一个方法CreateDynamicGenericInstance(Type keyType, Type valueType)
个它创建并返回一个IDictionary示例,其键和值的类型与调用
keyType
和valueType
中指定的类型完全相同。这里有一个完整的例子如何调用此方法示例化并使用
Dictionary<String, int>
:型
当执行上面的控制台应用程序时,我们得到正确的预期结果:
型
bfnvny8b7#
这是我基于Grax的答案的2美分,但是对于泛型方法需要两个参数。
假设您的方法在Helpers类中定义如下:
字符串
在我的例子中,U类型始终是一个存储类型为T的对象的可观察集合。
因为我已经预定义了类型,所以我首先创建了表示可观察集合(U)和存储在其中的对象(T)的“dummy”对象,下面将在调用Make
型
然后调用GetMethod来查找泛型函数:
型
到目前为止,上面的调用与上面解释的几乎相同,但在需要向其传递多个参数时有一点小的区别。
你需要传递一个Type[]数组给MakeGenericMethod函数,它包含上面创建的“dummy”对象的类型:
型
一旦完成,您需要调用上面提到的Invoke方法。
型
你完了很有魅力!
更新:
正如@Bevan所强调的,当调用MakeGenericMethod函数时,我不需要创建一个数组,因为它接受参数,我不需要创建一个对象来获取类型,因为我可以直接将类型传递给这个函数。在我的例子中,因为我在另一个类中预定义了类型,所以我简单地将代码改为:
型
myClassInfo包含2个
Type
类型的属性,我在运行时根据传递给构造函数的枚举值设置,并将为我提供相关类型,然后我在MakeGenericMethod中使用。再次感谢你突出这个@Bevan。
ruyhziif8#
受Enigmativity's answer启发-假设你有两个(或更多)类,比如
字符串
你想用
Bar
和Square
调用方法Foo<T>
,声明为型
然后你可以实现一个Extension方法,比如:
型
有了这个,你可以简单地调用
Foo
,像这样:型
它适用于每个班级。在这种情况下,它将输出:
正方形
酒吧
0lvr5msh9#
尽管这是一个相当古老的问题,但我发现它很有趣,因为有几个选项可以动态调用方法。从字面上看,它是反射、表达式树和发射器。从历史上看,反射是最慢的选择,而发射器是最快的选择。因此,我决定在这个有趣的案例中比较它们,看看现在是否有任何变化。最初的问题是问当类型参数在编译时未知时调用泛型方法的最佳方式。然而,上面几乎所有的答案都建议使用反射。
我已经为所有提到的方法创建了三个测试用例。首先,这里是一个稍微修改过的示例类,将使用3个方法进行测试:TestReflection、TestExpression和TestEmit。
字符串
类CallViaReflection表示通过反射调用泛型方法的帮助器类。我决定引入一个缓存以获得更好的结果。
型
下一个类CallViaExpression使用缓存的表达式树。
型
最后但并非最不重要的CallViaEmit发出必要的操作。
型
最后,这里是一个测试类和测试结果。
型
通过emit动态调用方法花费了2939毫秒。通过表达式树动态调用方法花费了3910毫秒。通过反射动态调用方法花费了6381毫秒。
显然,赢家是发射器。它的执行速度仍然快两倍以上。第二位是表达式树。
因此,我的判决在第二个十年里没有改变。如果你需要动态调用一个方法,从表达式树开始。如果您的代码是性能关键型的,请使用ILGenerator并发出必要的调用。尽管第一眼看上去可能很复杂,但所有必要的MSIL指令都可以使用ildasm轻松分解。
源代码可在GitHub上找到。