详解.NET 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 a=7;a.Error=”Error”;a=”Test”;a.Run();
- }}
这段代码可以通过编译,但无法运行。C#编译器允许你对a对象调用任何方法或其他成员,它并不会在编译时检查这些成员调用是否合法,取而代之的是,编译器会在运行时检查实际的对象是否具有相应的方法,如果有,则调用,否则,CLR会抛出异常。如,下面的代码将可以正常执行:
- static dynamic Sum(dynamic obj1,dynamic obj2){
- return obj1.Age+obj2.Age;
- }
- static void main(){
- var animal=new{Sex=”Male”,Age=”5”};
- var plant=new{Class=”草本”,Age=100};
- dynamic ageCount=Sum(animal+plant);}
这里我们对两个不同对象的年龄相加,在sum函数中,我们根本就不关心我们调用的对象是什么,而仅仅需要知道他们都有Age成员,并且这个成员能够进行+操作符运算。事实上,在与DLR的交互和Silverlight中,这种场景将会大量存在,因此dynamic在这些场合将会非常有用。
探讨玩使用情况之后我们再来看看dynamic到底是如何实现的。实际上通过Reflector查看代码你会发现它显示的代码是这样的:
- internal class Program
- { // Methods private static void Main(string[] args)
- { object a = 7;
- if (<Main>o__SiteContainer0.<>p__Site1 == null) {
- <Main>o__SiteContainer0.<>p__Site1 =
- CallSite<Func<CallSite, object, string, object>>.Create
- (Binder.SetMember(CSharpBinderFlags.None, "Error", typeof(Program), new CSharpArgumentInfo[]
- { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None,
- null), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.LiteralConstant | CSharpArgumentInfoFlags.UseCompileTimeType,
- null) }));
- } <Main>o__SiteContainer0.<>p__Site1.Target
- (<Main>o__SiteContainer0.<>p__Site1, a, "Error");
- a = "Test";
- if (<Main>o__SiteContainer0.<>p__Site2 == null)
- {
- <Main>o__SiteContainer0.<>p__Site2 = CallSite<Action<CallSite, object>>.Create(Binder.InvokeMember
- (CSharpBinderFlags.ResultDiscarded, "Run", null, typeof(Program), new CSharpArgumentInfo[]
- { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }));
- } <Main>o__SiteContainer0.<>p__Site2.Target(<Main>o__SiteContainer0.<>p__Site2, a);
- } // Nested Types [CompilerGenerated] private static class <Main>o__SiteContainer0
- { // Fields public static CallSite<Func<CallSite, object, string, object>> <>p__Site1;
- public static CallSite<Action<CallSite, object>> <>p__Site2;
- }}
大家可以可以从代码中看到,实际上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.
- class MyList<T>{
- public List<T> Items { get; set; }
- public T Sum() {
- dynamic result = default(T);
- foreach (var temp in Items)
- {
- result += temp; }
- return result; }}
这里由于我们将Result声明为dynamic类型,所以编译器不会检查其是否能进行+操作,但这里我们有个契约就是这个求和函数中的类型应支持+运算。现在我们可以对这个类进行如下操作:
- MyList<int> l1=new MyList<int>()…dynamic a= l1.Sum();
- MyList<String> l2=new MyList<String>()…. a=l2.Sum();
另外一个就是XML操作了,由于XML中所有的属性都是string类型的,但有时我们又却是需要使用其实际类型,这时dynamic也很有用。这里给出我看到的一个认为不错的例子,你可以参看这篇文章:
首先我们定义一个继承DynamicObject的动态类型。
- public class DynamicXMLNode : DynamicObject{
- XElement node;
- public DynamicXMLNode(XElement node) {
- this.node = node;
- }
- public DynamicXMLNode()
- { }
- public DynamicXMLNode(String name)
- { node = new XElement(name);
- }
- public override bool TrySetMember(
- SetMemberBinder binder, object value)
- {
- XElement setNode = node.Element(binder.Name);
- if (setNode != null)
- setNode.SetValue(value);
- else {
- if (value.GetType() == typeof(DynamicXMLNode))
- node.Add(new XElement(binder.Name));
- else
- node.Add(new XElement(binder.Name, value));
- }
- return true;
- }
- public override bool TryGetMember(GetMemberBinder binder, out object result)
- {
- XElement getNode = node.Element(binder.Name);
- if (getNode != null)
- {
- result = new DynamicXMLNode(getNode);
- return true; }
- else {
- result = null;
- return false;
- } }}
定义好动态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.Value+var2.Value);
- }}
然后我为了免去重复初始化列表的过程,我简单将普通方法调用,Dynamic方式调用和反射调用设计成一个嵌套类,见代码:
- public class MyTest{
- public static List<MyData> Items
- { get; set;
- }
- public MyTest(int count)
- { Items = new List<MyData>(count);
- for (int i = 0; i < count; i++) { Items.Add(new MyData(1));
- } }
- public void Run()
- {
- Console.WriteLine("Compare Times:
- {0}",Items.Count);
- CodeTimer.Time("Common", 1, new TestCommon());
- CodeTimer.Time("Dynamic", 1, new TestDynamic());
- CodeTimer.Time("Reflect", 1, new TestReflect());
- }
- public class TestCommon : IAction
- { public MyData Result { get; set; }
- public void Run()
- { Result = default(MyData); foreach (var d in Items)
- { Result += d;
- } } }
- public class TestDynamic : IAction
- { public dynamic Result { get; set; } public void Run()
- {
- Result = default(dynamic);
- foreach (dynamic d in Items)
- { Result += d;
- } }
- }
- public class TestReflect : IAction
- { public MyData Result { get; set; }
- public void Run()
- { Result = default(MyData);
- Type type = typeof(MyData);
- MethodInfo m = type.GetMethod("op_Addition", BindingFlags.Public | BindingFlags.Static);
- foreach (var d in Items) {
- Result = (MyData)(object)m.Invoke(null, new object[] { Result, d });
- } } }}
最后是调用方法:
- static void main(){
- MyTest test = new MyTest(100000);
- test.Run();
- test = new MyTest(1000000);
- test.Run();
- test = new MyTest(10000000);
- test.Run();}
最后我将给出测试结果,不过我发现每次测试结果数据好像都有所不同,但数据规律大致相似。
普通方法 |
动态调用 |
反射 |
|
数组大小 |
100,000 |
||
Time Elapsed |
9ms |
274ms |
442ms |
Time Elapsed (one) |
9ms |
274ms |
442ms |
CPU time |
15,625,000ns |
296,875,000ns |
484,375,000ns |
CPU Time (one) |
15,625,000ns |
296,875,000ns |
484,375,000ns |
Gen 0 |
0 |
1 |
5 |
Gen 1 |
0 |
0 |
0 |
Gen 2 |
0 |
0 |
0 |
数组大小 |
1,000,000 |
||
Time Elapsed |
42ms |
244ms |
3,736ms |
Time Elapsed (one) |
42ms |
244ms |
3,736ms |
CPU time |
62,500,000ns |
281,250,000ns |
4,140,625,000ns |
CPU Time (one) |
62,500,000ns |
281,250,000ns |
4,140,625,000ns |
Gen 0 |
0 |
7 |
20 |
Gen 1 |
0 |
0 |
0 |
Gen 2 |
0 |
0 |
0 |
数组大小 |
10,000,000 |
||
Time Elapsed |
585ms |
2,553ms |
40,763ms |
Time Elapsed (one) |
585ms |
2,553ms |
40,763ms |
CPU time |
656,250,000ns |
2,796,875,000ns |
43,671,875,000ns |
CPU Time (one) |
656,250,000ns |
2,796,875,000ns |
43,671,875,000ns |
Gen 0 |
0 |
30 |
205 |
Gen 1 |
0 |
1 |
4 |
Gen 2 |
0 |
0 |
0 |
从表格中我们大致可以看出,直接调用方法最快,并且产生的对象最少。通过反射方式不仅时间往往耗费较多,而且还会生产大量的对象。另外我们发现在1,000,000反而比100,000花费的时间要少,但生成的对象确实增多了。这一点我不太明白,同样的对象通过Cache确实能提高效率,但我不知道为什么多做10倍的加法操作的动态方法调用反而会更快。另外,从图中也可以看出基本上使用dynamic调用方法花费的时间是直接调用的5倍左右,在有些时候,这个性能损失所做的交换也是值得的。
上面的测试数据基本每次都会有所变化,但总体走势是基本不变的,那就是花费时间common>dynamic>Reflect。因此我们可以认为虽然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
结果截图:
机器信息:
Thinkpad X200 7654;CPU:Intel Core2 Duo CPU p8400 2.25GHz;虚拟机:VMWare 6.5;虚拟内存:1G;虚拟机操作系统:Windows XP;编译器版本:Visual C# Express 2010 Beta2。