前段時間看過一些關於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。