C# 4.0 中的 dynamic 关键字






4.69/5 (38投票s)
对 'dynamic' 关键字的介绍。
引言
本文是系列文章的第二篇,介绍了 C# 4.0 的一些新语言特性。我的第一篇文章介绍了 命名参数和可选参数[^]。值得重申的是,本系列文章旨在提供对语言特性的简明介绍,而不是深入分析,特别适合 C# 4.0 和 C# 的初学者。
正如在第一篇文章中所讨论的,C# 4.0 的主要目标之一是增加对框架中动态语言的支持;当然,dynamic
关键字起着重要作用。我无意讨论 DLR,因为它在与动态语言打交道时大部分是不可见的,而我对动态语言的经验很少。
dynamic 类型有什么用?
dynamic
是一种新的静态类型,它充当一个占位符,表示一个类型直到运行时才知晓。一旦声明了 dynamic
对象,就可以对其进行操作、获取和设置属性,甚至几乎可以像任何普通类型一样传递 dynamic 实例。这看起来是相当标准的用法
//This could be the result of call to Python / Ruby etc !
dynamic myDynamic = GetDynamic(....);
myDynamic.Foo("Hello"); //Call a method
myDynamic.Bar = myDynamic.Baz; //Using properties
int Quux = myDynamic[0]; //calling on an indexer
int Qux = myDynamic(12,5); //Invoking as delegate
MyMethod(myDynamic); // Passing as parameter
动态支持可能是 C# 4.0 中最具争议的新特性。我听说有同行开发者抱怨它破坏了 C# 的面向对象特性,因为它提供了一种在编译时定义“未知”类型并在其上执行“未知”操作的方法。我承认,在我开始接触这些特性之前,我也持同样的观点。那么为什么会有争议呢?类型在运行时才可知(取决于与什么交互,类型可能是在运行时动态生成的!),这使得 CLR 编译器无法知道 dynamic
对象上的操作是否存在(这与 var
关键字不同)。
例如,如果动态创建的对象没有一个接受 string
的名为 Foo
的方法,那么上面的代码将会编译,但在执行时会抛出 RuntimeBinderException
。问题就出在这里。我们现在有了一个绕过 C# 正常严格类型的机制。现在可以编译一些在编译时如果经过彻底检查则不会通过的代码。此外,如果误用 dynamic,会使重构更加困难。例如,如果 `GetDynamic(....)` 调用中创建的对象被重构以重命名其 `Foo(string s)` 方法,我代码片段中的代码不会自动更改;事实上,它仍然可以编译。
为什么要使用 dynamic?
在静态类型世界中,dynamic
为开发人员提供了很大的自由度。当处理类型在编译时可知的情况下,应不惜一切代价避免使用 dynamic
关键字。前面我说过我最初的反应是负面的,是什么改变了我的想法?引用玛格丽特·阿特伍德的话来说,语境就是一切。在静态类型处理中,dynamic 毫无意义。如果你处理的是未知或动态类型,通常需要通过反射与其进行通信。反射代码不易阅读,并且具有上述 dynamic 类型所有的弊端。在这种情况下,dynamic
就非常有意义了。
在底层,dynamic 类型会编译成包装的反射代码,除非该对象是 COM 对象(在这种情况下,操作通过 IDispatch
动态调用)或该对象实现了 IDynamicObject
(IronPython 和 IronRuby 以及一些新兴 API 大量使用该接口)。IDynamicObject
的子类会被要求直接执行操作,从而可以重新定义动态操作。从这个角度来看,dynamic 是 C# 工具集的一个极佳补充。“实际示例”部分提供了一个重写反射代码的演示。但首先,让我们看看 dynamic 对象的更一般属性。
赋值和声明
对象可以直接赋值(并隐式转换)
dynamic d = "Hello World"; //Implicit conversion
string s = d; //Conversion at Assignment
如上所述,dynamic 对象可以声明为某个方法的返回值
dynamic d = GetDynamicObject(...);
以下代码将无法编译,会报告使用了未赋值的变量 message
dynamic d;
dynamic type = d.GetType();
常见的 RuntimeBindingException
现在是时候看看 RuntimeBindingException
了。通常,当调用的操作在运行时无法解析时,就会发生这些异常。以下代码可以编译,但在执行时会抛出 RuntimeBindingException
,具体描述在注释中
//Attempting to access null reference
dynamic nulled = null;
dynamic type = nulled.GetType();
//Cannot perform runtime binding on null reference.
//Accessing a non-existant property
dynamic foo = 7;
dynamic baz = myInt.Bar; //'int' does not contain a definition for 'Bar'
//Accessing a non-existant method
dynamic foo = 7;
dynamic baz = myInt.Baz(); //'int' does not contain a definition for 'Baz'
//Bad implicit conversion
dynamic foo = "Hello";
int bar = foo; // Cannot implicitly convert type 'string' to 'int'.
上述代码的静态类型版本将无法编译,因此当类型在编译时已知时,dynamic 代码会更弱。但如果与反射调用等进行比较,异常机制会更整洁、对开发人员更透明。
重载解析
Foo foo = new Foo();
dynamic myDynamic = new Bar();
var returnedValue = foo.Baz(myDynamic);
运行时将根据 `myDynamic` 的运行时类型来解析 `Foo` 中 `Baz` 的重载。请注意,不需要动态和非动态参数之间的重载解析,因为以下代码是无效的
public class Foo
{
public void Bar(AnyType baz)
{
}
//Won't compile as conflicts with first overload
public void Bar(dynamic baz)
{
}
}
dynamic 的局限性
- 无法调用扩展方法
- 无法调用匿名函数(lambda)
- 由于上述原因,LINQ 支持有限;请在 MSDN 上查看仅
List<T>
的扩展方法;所有这些方法在dynamic
实例上都不可用
实际示例
我的实际示例(可在本文附带的解决方案中找到)展示了如何使用 dynamic
来改进一个代码库,其中对对象的反射调用操作(方法调用和 get
访问器)被等效的基于 dynamic 的代码替换。
测试框架
我的解决方案有一个非常简单的对象模型。有两个类,Class1
和 Class2
,它们实现了以下接口
public interface IBaseClass
{
string Property { get; set; }
void Method();
}
在实现的类中,调用 Method
会将“Method Called on ClassName”写入控制台,而访问 Property
的 getter 会返回“Property of ClassName”。
在解决方案中,有一个 InstanceCreator
类,它通过调用相关构造函数来返回 IBaseClass
类型的实例化对象。这是为了支持一组使用直接操作 POCO 调用进行测试,这需要已知类型的具体实例。由 InstanceCreator
返回的对象可以被认为来自 COM 或动态源,用于 dynamic 测试(但要注意它们没有实现相应的 IDispatch
或 IDynamicObject
接口)。
最后,有三个测试器类代表三种执行类型。StaticTester
以静态类型 POCO 的方式调用对象,RelectiveTester
模拟在运行时以未知类型仿佛反射创建的对象调用,而 DynamicTester
用更简洁的 dynamic
代码替换了反射代码。请注意,在源代码中,对于反射和 dynamic 示例,实例化可以采用反射方式进行,因为它们是类型无关的。
静态调用
没有意外
public class StaticTester : Tester
{
void WriteClassDetails(IBaseClass instance)
{
instance.Method();
Console.WriteLine(instance.Property);
}
}
方法和属性直接调用。请注意,无法通过这种方式调用不存在的属性或方法,因为代码将无法编译,因为 CLR 编译器知道类型。
通过反射调用
现在,让我们假设我们需要通过反射调用操作,因为对象类型直到运行时才知道。由于属性和方法是通过它们的名称(表示为字符串)调用的,因此可以尝试调用一个不存在的操作。已添加代码来防止这种情况(用斜体表示)
public class ReflectiveTester : Tester
{
static void WritePropertyReflectively(object instance, string propertyName)
{
Type type = instance.GetType();
PropertyInfo propertyInfo = type.GetProperty(propertyName);
if (propertyInfo == null)
Console.WriteLine("Property \"{0}\" not found, propertyInfo " +
"is null\r\npropertyInfo.GetValue(...) will result " +
"in a NullReferenceException", propertyName);
else
Console.WriteLine(propertyInfo.GetValue(instance, null));
}
static void CallMethodReflectively(object instance, string methodName)
{
Type type = instance.GetType();
MethodInfo methodInfo = type.GetMethod(methodName);
if (methodInfo == null)
Console.WriteLine("Method \"{0}\" not found, " +
"methodInfo set to null\r\nmethodInfo.Invoke(...) " +
"will result in a NullReferenceException", methodName);
else
methodInfo.Invoke(instance, null);
}
static void WriteClassDetails(object instance)
{
Type type = instance.GetType();
WritePropertyReflectively(instance, "Property");
CallMethodReflectively(instance, "Method");
}
}
请注意,现在代码的行为不像反射代码那样明显,“隐藏”了它。开发者也经常错误地忘记保护以防止“找到”的 PropertyInfo
或 MethodInfo
为 null
。在这种情况下进一步执行将导致 NullReferenceException
。这可能会误导不明就里的人,因为经常会传递可能引发此错误类型的参数,尽管在本例中这些参数为 null
。此外,我的示例稍微简化了防御性代码,通常需要抛出并捕获异常。
使用 dynamic
现在,我们将使用 dynamic
,实现与反射代码相同的功能。同样,对象类型直到运行时才知道,但代码非常接近我们的第一个示例。通过调用方法和属性,我们可以清楚地知道意图是要调用什么,就好像它们在运行时就已经知道一样。但是,我们仍然需要进行防御性编程,因为我们不确定所传入 `instance` 上的操作是否存在。幸运的是,我们只需要担心 RuntimeBinderException
,这使得代码更简洁。
public class DynamicTester : Tester
{
void WriteClassDetails(dynamic instance)
{
try
{
Console.WriteLine(instance.Property);
instance.Method();
}
catch (RuntimeBinderException ex)
{
ErrorWriters.WriteRuntimeBinderException(ex);
}
}
}
有一点需要说明的是,这并不比反射调用“不面向对象”;它直接等效,但具有简洁的语法和不那么晦涩的异常机制。从这个角度来看,添加 dynamic
是一个巨大的好处,即使在不考虑动态语言支持的情况下也是如此。
结论
Dynamics 是一个强大的新工具,可以简化与动态语言以及 COM 的互操作,并且可以用来替代大量繁琐的反射代码。它们可以用来告知编译器对对象执行操作,而这些操作的检查被推迟到运行时。
最大的危险在于在不恰当的上下文中(例如,在静态类型系统中,或者更糟的是,在正确类型的系统中代替接口/基类)使用 dynamic 对象。
关于解决方案的说明
解决方案提供了此处示例的源代码。它是使用 VS2010 RC1 创建的;当然,您必须拥有 C# 4.0 才能编译。该解决方案是一个简单的控制台应用程序,它运行了三种主要代码变体的测试:调用普通 C 对象的方法、通过反射调用以及使用 dynamics。`Program.cs` 中的一个方法包含多个注释掉的坏代码和无法编译的代码示例,可以取消注释并执行以查看其效果。有关详细信息,请参阅文件。
历史
- 2010 年 3 月 29 日:创建文章。
- 2010 年 3 月 31 日:修正了文章标题中的错误。