原文转自:C#4.0新特性(1):Dynamic Lookup 动态查找
C# 4.0的主要主题是动态编程。对象的意义变得越来越“动态”,它们的结构和行为无法通过静态类型来捕获,或者至少编译器在编译程序时无法得知对象的结构和行为。例如——
- a. 来自动态编程语言——如Python或Ruby——的对象
- b. 通过IDispatch访问的COM对象
- c. 通过反射访问的一般.NET类型
- d. 结构发生过变化的对象——如HTML DOM对象
动态查找允许在编写方法、运算符和索引器调用、属性和字段访问甚至对象调用时,绕过C#静态类型检查,而在运行时进行解析。
动态查找允许动态(即在运行时)实现对某个对象的操作与对象类型的绑定,而不管这个对象是来自COM,IronPython,HTML DOM还是CLR的反射。你可以在程序中绕过编译器的类型检查,而把类型的匹配(lookup)丢给运行时去作。如果你需要对这样的对象进行操作,则会用到一个全新的类型:dynamic。
dynamic类型(The dynamic type)
C# 4.0引入了一个新的静态类型,称为dynamic。当你拥有了一个dynamic类型的对象后,你“对他做的事情”只会在运行时进行解析——
1 | dynamic d = GetDynamicObject(...); |
C#编译器允许你使用任何参数在d上调用一个方法,因为它的类型是dynamic。运行时会检查d的实际类型,并检测在它上面“用一个int调用M”是什么意思。
可以认为dynamic类型是object类型的一个特殊版本,指出了对象可以动态地使用。选择是否使用动态行为很简单——任何对象都可以隐式转换为dynamic,“挂起信任”直到运行时。反之,从dynamic到任何其他类型都存在“赋值转换”,可以类似于赋值的结构中进行隐式转换——
动态操作(Dynamic operations)
不仅是方法调用,字段和属性访问、索引器和运算符调用甚至委托调用都可以动态地分派——
1 | dynamic d = GetDynamicObject(…); |
03 | static void Main( string [] args) |
10 | System.Console.WriteLine(dyn.GetType()); |
11 | System.Console.WriteLine(obj.GetType()); |
程序输出为:
dynamic关键字还可以作为类型的属性、字段、索引器、参数、返回值、局部变量和类型约束。请看下面示例:
07 | dynamic prop { get ; set ; } |
10 | public dynamic exampleMethod(dynamic d) |
13 | dynamic local = "Local variable" ; |
还可以作为显式类型转换中的目标类型,如下:
01 | static void convertToDynamic() |
08 | string s = "Example string." ; |
12 | DateTime dt = DateTime.Today; |
还可以用于is、as和typeof运算符:
06 | if (someVar is dynamic) { } |
12 | Console.WriteLine( typeof (List<dynamic>)); |
类型转换:
2 | dynamic d2 = "a string" ; |
3 | dynamic d3 = System.DateTime.Today; |
4 | dynamic d4 = System.Diagnostics.Process.GetProcesses(); |
4 | System.Diagnostics.Process[] procs = d4; |
完整示例如下:
03 | namespace DynamicExamples |
07 | static void Main( string [] args) |
09 | ExampleClass ec = new ExampleClass(); |
10 | Console.WriteLine(ec.exampleMethod(10)); |
11 | Console.WriteLine(ec.exampleMethod( "value" )); |
17 | dynamic dynamic_ec = new ExampleClass(); |
18 | Console.WriteLine(dynamic_ec.exampleMethod(10)); |
30 | dynamic prop { get ; set ; } |
32 | public dynamic exampleMethod(dynamic d) |
34 | dynamic local = "Local variable" ; |
运行时查找(Runtime lookup)
在运行时,动态操作将根据目标对象d的本质进行分派——
COM对象(COM objects)
如果d是一个COM对象,则操作通过COM IDispatch进行动态分派。这允许调用没有主互操作程序集(Primary Interop Assembly,PIA)的COM类型,并依赖C#中没有对应概念的COM特性,如索引属性和默认属性。
下面的代码动态的获取一个 COM 对象类型,并通过该类型创建这个 COM 对象的实例,并准备调用该实例上的一个方法实现我们需要的功能。这个例子引用了 Speech API 中的 SAPI.SpVoice 对象,并调用了其 Speak() 方法。
using System;
Type type = Type.GetTypeFromProgID("SAPI.SpVoice");
dynamic spVoice = Activator.CreateInstance(type);
spVoice.Speak("Hello, C# 4.0!");
编译并运行此示例,我们通过计算机的音箱得到了正确的语音。
现在我们开始思考一个问题,spVoice.Speak(string) 这个函数签名其实在设计时以及编译时对于 C# 编译器来说都是未知的。因为 spVoice 变量的类型是 System.__ComObject,并且由于 SAPI.SpVoice 对应的 ProgID 指向一个非托管的 COM 对象,加上我们并没有导入任何 TLB 库,因此,Speak() 方法只能是在运行时由编译器自动绑定。
这就是动态查找最基本的意图,它的提出是为了在 Office 以及 COM 互操作性编程中更加简化代码。dynamic 变量的所有运行时类型信息都直接由运行时上下文绑定并执行(称之为“晚期绑定”),这些工作通过 System.Dynamic.RuntimeBinding 类完成。
动态对象(Dynamic objects)
如果d实现了IDynamicObject接口,则请求d自身来执行该操作。因此通过实现IDynamicObject接口,类型可以完全重新定义动态操作的意义。这在动态语言——如IronPython和IronRuby——中大量使用,用于实现他们的动态对象模型。API也会使用这类对象,例如HTML DOM允许直接使用属性语法来访问对象的属性。
简单对象(Plain objects)
除此之外,则d是一个标准的.NET对象,操作是通过在其类型上进行反射来分派的,C#的“运行时绑定器(runtime binder)”实现了运行时的C#查找和重载解析。其背后的本质是将C#编译器作为运行时组件运行,来“完成”被静态编译器延迟的动态操作。
考虑下面的代码——
1 | dynamic d1 = new Foo(); |
2 | dynamic d2 = new Bar(); |
由于对M进行调用的接受者是dynamic类型的,C#编译器不会试图解析该调用的意义。而是将有关该调用的信息存储起来,供运行时使用。该信息(通常称作“有效载荷”)本质上等价于——
“使用下面的参数执行一个称作M的实例方法——
- 1. 一个string
- 2. 一个dynamic
- 3. 一个int字面值3
- 4. 一个object字面值null”
在运行时,假设d1的实际类型Foo不是COM类型,也没有实现IDynamicObject。在这种情况下,C#运行时绑定器担负起了重载解析的工作,这是基于运行时类型信息完成的,按照下面的步骤进行处理——
- 1. 使用反射获取两个对象d1和d2的实际运行时类型,它们没有静态类型(包括静态类型dynamic)。结果为d1是Foo类型而d2是Bar。
- 2. 使用普通的C#语义在Foo类型上对M(string,Bar,3,null)调用进行方法查找和重载解析。
- 3. 如果找到了该方法,则调用它;否则抛出运行时异常。
带有动态参数的重载解析(Overload resolution with dynamic arguments)
2 | ec.exampleMethod2( "a string" ); |
即便方法调用的接受者是静态类型的,重载解析依然发生在运行时。当一个或多个实参是dynamic类型时就会出现这种情况——
C#运行时绑定器会基于d的运行时类型——也就是Bar——在Foo上M方法的静态可知(statically known)重载之间进行选择。其结果是dynamc类型。
动态语言运行时(The Dynamic Language Runtime)
动态语言运行时(Dynamic Language Runtime,DLR)是动态查找的底层实现的一个重要组件,也是.NET 4.0中新增的API。DLR不仅为C#动态查找,还为很多其他.NET上的动态语言——如IronPython和IronRuby——的实现提供了底层的基础设施。这一通用基础设施确保了高度的互操作性,更重要的是,DLR提供了卓越的缓存机制,使得运行时分派的效率得到巨大的改善。
对于使用C#动态查找的用户来说,除了更高的性能之外,根本感觉不到DLR的存在。不过,如果你希望实现自己的动态分派对象,可以使用IDynamicObject接口来与DLR互操作,并向其中插入自己的行为。这是一个非常高级的任务,要求对DLR的内部工作原理有相当深入的了解。对于编写API的人,值得在这些问题上花些功夫,这样能够更广泛地改善可用性,例如为一个本身就是动态的领域编写类库。
已知问题(Open issues)
- DLR允许从一个表示类的对象创建对象。然而,C#的当前实现还不具备支持这一功能的语法。
- 动态查找不能查找扩展方法。不论扩展方法是否依赖该调用的静态上下文(也就是出现了using语句),因为该上下文信息并不会作为有效载荷的一部分保留下来。
- 匿名函数(也就是lambda表达式)不能作为实参传递给动态方法调用。在不知道要转换成什么类型的情况下,编译器不能绑定(也就是“理解”)一个匿名函数。
这些限制导致的结果就是很难在动态对象上使用LINQ查询——
1 | dynamic collection = ...; |
3 | var result = collection.Select(e => e + 5); |
如果Selected方法是个扩展方法,动态查找将找不到它。即便它是一个实例方法,上面的代码也无法编译,因为lambda表达式不能作为参数传递给动态操作。
在C# 4.0中没有计划解决这些限制。
注意:dynamic 不同于 object。object 是任何类的基础类,它是一个强类型的 CLR 类型,是引用类型。它具备完整的运行时信息,任何企图访问 object 不存在的方法签名或者属性都会被编译时由编译器检查出来并报告错误。dynamic 类型在编译时可能是未知的,所有针对它类型上的调用在编译时是不会报告错误的,只有在运行时由 DLR 去检查这些信息后,才能确定一个调用是否成功或者失败。
由于 dynamic 的这些特性,使得它广泛用于 COM 以及其他互操作编程中,它为我们的代码带来了简化和便利,但同时由于这个口子已经被打开,所有强类型都可以被转化为 dynamic,所以它如果被滥用,则会对代码的维护难度有很大的提高。因为编译器这个时候并不能在编译时检查更多的错误了。所以我们强烈建议您除了在互操作方面应用 dynamic 外,不要在其他地方滥用,以免对现有代码产生潜在的维护成本。