C# 4.0 新特性-dynamic 前段时间看过一些关于dynamic这个C#4中的新特性看到有些朋友认为dynamic的弊大于利如无法使用编译器智能提示无法在编译时做静态类型检查性能差等等。因此在这篇文章中我将就这些问题来对dynamic做一个较详细的介绍希望通过这篇文章能使大家对dynamic关键字有个更深入的认识。dynamic介绍相信很多人应该都已经对Anders Hejlsberg在PDC2008上所做的那篇”The Future of C#”(注1) 都有所了解了当时的这篇演讲已经介绍了C#4.0的一些最重要的特性。Anders提到C#的未来时候指出C#4.0的特点是动态编程他同时也列举了很多在4.0中关于动态编程的例子这里我具体讲一讲他首先提到的dynamic关键字。提到dynamic我首先想到的是var关键字。事实上当var在C#3.0中刚刚出现的时候就引起了一些人的质疑后来微软解释var只是隐含类型声明符并且只能用作局部变量它其实仍然是强类型只不过是编译器由初始化结果推断而来所以对这个变量仍然可以可以使用VS的只能提示。现在dynamic则真正往动态特性迈进了一大步根据Anders的解释dynamic是指动态的静态类型也就是说它本质上仍然是静态类型只不过它告诉编译器忽略对它的静态类型检查它会在运行时才进行类型检查它可以应用在基本上所有的C#类型上面如方法操作符索引器属性字段它其实是通过统一的方式来调用方法、属性等操作。dynamic主要用与需要与外界(COM,DLR,HTML DOM,XML等)的交互的场合在这些时候你很可能不能确定这些对象的具体类型而仅仅知道它的一些属性如方法等因此这些时候你仅仅告诉编译器你需要在程序运行这里执行这些方法至于操作对象是什么你可能并不关心。这个时候静态类型无法帮你解决问题因为它们是在编译时就已经决定了的反射虽然能做大但毕竟太麻烦而且效率较低。因此dynamic适时的出现了它用编译时类型检查缺失的代价来实现让程序员看起来很干净的代码。dynamic的声明和使用很简单跟javascript中的var基本是一致的。需要注意的是在编译代码之前我们首先需要安装VS 2010 Beta2或Visual C# Express 2010我这里安装的是C# Express(注2)。e.g.代码1:class Program{static void main(){dynamic a7;a.Error”Error”;a”Test”;a.Run();}}这段代码可以通过编译但无法运行。C#编译器允许你对a对象调用任何方法或其他成员它并不会在编译时检查这些成员调用是否合法取而代之的是编译器会在运行时检查实际的对象是否具有相应的方法如果有则调用否则CLR会抛出异常。如下面的代码将可以正常执行代码这里我们对两个不同对象的年龄相加在sum函数中我们根本就不关心我们调用的对象是什么而仅仅需要知道他们都有Age成员并且这个成员能够进行操作符运算。事实上在与DLR的交互和Silverlight中这种场景将会大量存在因此dynamic在这些场合将会非常有用。探讨玩使用情况之后我们再来看看dynamic到底是如何实现的。实际上通过Reflector查看代码你会发现它显示的代码是这样的代码大家可以可以从代码中看到实际上dynamic对象就是object对象在编译的时候编译器会给每个不同的方法在类的嵌套静态类SiteContainer生成不同的CallSite字段这些CallSite会将绑定调用方法的信息当需要真正调用方法的时候它会调用由编译器生成的嵌套静态类SiteContainer中的CallSite来调用实际方法这里CLR通过将调用的方法设置为静态变量来达到Cache的目的也就是如果该方法是第一次调用那么它会创建该类型否则它会直接调用之前生成的静态CallSite类型来调用实际方法。这对大批量重复操作来说可以显著提高效率。我将在后文对此进行详细测试。使用举例好了介绍完dynamic之后我们来讨论下这个新特性的使用场景吧关于dynamic的使用例子其实Anders 在他的演讲中已经展示了很多例子。我这里首先对这些例子做个总结1. SilverLight中与javascript交互在视频中他不仅演示了我们如何调用HTML中的Javascript 方法Anders甚至给我们演示了如何直接在C#代码中加入Javascipt方法;2. 这个例子是C#和动态语言IronPython交互的情况在这个例子中他演示我们如何直接调用一个在Python中定义的Calculate方法。3. 除了这些他还演示了通过Dynamic干净直观的操作XML。这里我额外补充两个使用dynamic的例子实际上这只是我提供的一种思路如果你觉得他们的实现并不好我很欢迎你提出不同的意见或更恰当的例子。1. 让泛型支持操作符重载。我曾经在复习泛型的时候提到过.NET泛型是不支持操作符的因为操作符是编译器决定的而泛型是运行时决定。所以如果你想对两个泛型变量进行的操作是无法通过编译的。事实上在Linq实现Sum操作的时候也是通过对所有基本数据类型(e.g.int,long)的重载来实现的。但有了dynamic,这种操作将变得可能。e.g.代码这里由于我们将Result声明为dynamic类型所以编译器不会检查其是否能进行操作但这里我们有个契约就是这个求和函数中的类型应支持运算。现在我们可以对这个类进行如下操作MyListint l1new MyListint()…dynamic a l1.Sum();MyListString l2new MyListString()…. al2.Sum();另外一个就是XML操作了由于XML中所有的属性都是string类型的但有时我们又却是需要使用其实际类型这时dynamic也很有用。这里给出我看到的一个认为不错的例子你可以参看这篇文章首先我们定义一个继承DynamicObject的动态类型。代码定义好动态XML节点类之后我们可以像下面这样使用它。dynamic contact new DynamicXMLNode(Contacts);contact.Name Patrick Hines;contact.Phone 206-555-0144;contact.Address new DynamicXMLNode();contact.Address.Street 123 Main St;contact.Address.City Mercer Island;contact.Address.State WA;contact.Address.Postal 68402;是不是真正做到了XML对象和C#对象的无缝衔接了不足1. 无法支持扩展方法。由于扩展方法能否被加载是根据上下文如DLL的引用和命名空间的引用这些静态信息来获取的目前dynamic还不支持调用扩展方法。这也意味着Linq没办法被dynamic支持。2.无法支持匿名方法。匿名方法(Lamda表达式)无法作为一个动态方法调用的参数传递。编译器没办法获取一个匿名方法的具体类型所以它也就没办法绑定匿名方法了。性能很多朋友考虑dynamic一个很重要的缺点就是认为它本质还是object类型只不过CLR在运行时候通过反射来达到动态调用的目的。确实没错跟普通方法调用比较动态类型的方法在第一次调用的时候要做很多的事情它需要把调用的信息存放起来然后在真正用到这个方法的时候通过CallSite.Create()来调用实际的方法当然这个Create里面也是通过反射来达到目的的。不过这是否意味着dynamic还不如反射的性能呢答案是否的事实上看我上面的代码你会发现动态对象在每次调用方法的时候都会先判断这个callSite对象是否是空的如果不是空的它可以直接调用而不需要重新实例化所以如果你的对象的方法需要有很多重复使用的时候它的性能其实并不会太差。下面我将给出测试的代码。这里我的测试目标是对一个大型数组进行求和操作在这个测试中由于系统是XP我使用了装配脑袋写的性能计数器你可以参看对老赵写的简单性能计数器的修改。首先我需要定义一个支持操作符的结构(我本来想直接使用int但测试的时候不知为何int的相加运算符无法调用)public struct MyData{public int Value;public MyData(int value){this.Value value;}public static MyData operator (MyData var1,MyData var2){return new MyData(var1.Valuevar2.Value);}}然后我为了免去重复初始化列表的过程我简单将普通方法调用Dynamic方式调用和反射调用设计成一个嵌套类见代码代码最后是调用方法static void main(){MyTest test new MyTest(100000);test.Run();test new MyTest(1000000);test.Run();test new MyTest(10000000);test.Run();}最后我将给出测试结果不过我发现每次测试结果数据好像都有所不同但数据规律大致相似。普通方法动态调用反射数组大小100,000Time Elapsed9ms274ms442msTime Elapsed (one)9ms274ms442msCPU time15,625,000ns296,875,000ns484,375,000nsCPU Time (one)15,625,000ns296,875,000ns484,375,000nsGen 0015Gen 1000Gen 2000数组大小1,000,000Time Elapsed42ms244ms3,736msTime Elapsed (one)42ms244ms3,736msCPU time62,500,000ns281,250,000ns4,140,625,000nsCPU Time (one)62,500,000ns281,250,000ns4,140,625,000nsGen 00720Gen 1000Gen 2000数组大小10,000,000Time Elapsed585ms2,553ms40,763msTime Elapsed (one)585ms2,553ms40,763msCPU time656,250,000ns2,796,875,000ns43,671,875,000nsCPU Time (one)656,250,000ns2,796,875,000ns43,671,875,000nsGen 0030205Gen 1014Gen 2000从表格中我们大致可以看出直接调用方法最快并且产生的对象最少。通过反射方式不仅时间往往耗费较多而且还会生产大量的对象。另外我们发现在1,000,000反而比100,000花费的时间要少但生成的对象确实增多了。这一点我不太明白同样的对象通过Cache确实能提高效率但我不知道为什么多做10倍的加法操作的动态方法调用反而会更快。另外从图中也可以看出基本上使用dynamic调用方法花费的时间是直接调用的5倍左右在有些时候这个性能损失所做的交换也是值得的。上面的测试数据基本每次都会有所变化但总体走势是基本不变的那就是花费时间commondynamicReflect。因此我们可以认为虽然dynamic确实会有性能损失但有时候如果你的系统确实需要动态生成对象或动态调用方法的时候它还是可以考虑的特别是如果你系统需要使用反射的而这种类型的操作又会有多次重复的情况下尤其值得考虑。后记关于dynamic关键字目前还没有太多使用的用例关于它到底好还是不好的争论也一直没有停止。事实上一直到现在都有很多人对dynamic持有怀疑和反对态度他们认为dynamic性能并不好而且dynamic的到来使得编译器的自动完成功能没有了同时还无法完成编译时成员调用的检查这将使得普通程序员的出错几率大大增加。不否认dynamic使用不当确实会导致程序员犯错的几率大大提高。然而正如蜘蛛侠说的能力越大责任越大。其实微软引入dynamic也是给了C#程序员比以往更强大的能力但这强大的能力使用不当也会造成错误。不过我们能因此而说这个能力是丑恶的或者是坏的么我想能力其实没有好坏之分能力的好坏得看使用者例如核能同样的道理聪明的程序员可以把一个特性使用得优雅高效而愚蠢的程序员则恰好相反。其次对这些动态语言可能存在的问题来说我们也可以通过其他方式尽量来避免。首先类型或成员调用合法性的检查其实我们可以通过单元测试得到最大保证。对一个健壮的大型程序来说单元测试是必要的。其次使用dynamic之后我们确实缺少了智能提示但我并没有提倡将dynamic用在所有的地方(事实上那样也是错了因为它将造成程序的效率显著降低)我的意思是你仅仅在你真正需要使用dynamic的时候才去使用它这正是这个关键词存在的理由。而这些需要使用dynamic的地方不会很多我们也能明白在这些地方我们要用它来做什么。有了这两点保证我相信dynamic引起的不便也不会那么明显。最后dynamic只是微软给我们程序员的更多的一个选择如果你不喜欢它你当然可以在很多场合避免使用它比如使用类型转换反射等等。用还是不用它微软把选择权交给了我们程序员自己。另外你也可以观看来自C# Compiler Team更多关于Dynamic的介绍C# 4.0 Dynamic with Chris Burrows and Sam Ng附注注1: The future of C# ,Anders Hejlsberg, http://channel9.msdn.com/pdc2008/TL16/注2: Visual C# Express 2010下载地址http://www.microsoft.com/express/future/default.aspx参考资料New features in C# 4.0结果截图