前段时间看过一些关于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;
}
}
}