使用dynamic 类型而不是反射 API 可以大大简化使用仅在运行时已知的类型参数调用泛型方法。
要使用这种技术,必须从实际对象(不仅仅是Type 类的实例)中知道类型。否则,您必须创建该类型的对象或使用标准反射 API solution。您可以使用Activator.CreateInstance 方法创建对象。
如果你想调用一个泛型方法,在“正常”使用中会推断出它的类型,那么它只是将未知类型的对象转换为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 运算符)。
通过将对象参数转换为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 中的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 类型来执行一些方法。只有第一个参数类型的验证被推迟到运行时。如果您调用的方法的名称不存在或其他参数无效(参数数量错误或类型错误),您将收到编译器错误。
当您将dynamic 参数传递给方法时,此调用为lately bound。方法重载解析发生在运行时并尝试选择最佳重载。因此,如果您使用BarItem 类型的对象调用ProcessItem 方法,那么您实际上将调用非泛型方法,因为它更适合这种类型。但是,当您传递 Alpha 类型的参数时会出现运行时错误,因为没有可以处理此对象的方法(泛型方法具有约束 where T : IItem 和 Alpha 类不实现此接口)。但这就是重点。编译器没有此调用有效的信息。作为程序员的你知道这一点,你应该确保这段代码运行没有错误。
返回类型陷阱
当您使用动态类型的参数调用非 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 函数的返回值。