C# 4動態編程新特性與DLR剖析
近幾年來,在TIOBE 公司每個月發布的編程語言排行榜 [1] 中,C# 總是能擠進前10 名,而在近10 年的編程語言排行榜中,C# 總體上呈現上升的趨勢。C# 能取得這樣的成績,有很多因素在起作用,其中,它在語言特性上的銳意進取讓人印象深刻( 圖1 )。
圖1 C#各版本的創新點
2010 年發布的 C# 4 ,最大的創新點是擁有了動態編程語言的特性。
1 動態編程語言的中興
動態編程語言并非什么新鮮事物,早在面向對象編程語言成為主流之前,人們就已經使用動態編程語言來開發了。即使在 Java 、 C# 、 C++ 等面向對象編程語言繁榮興旺、大行于世的年代,動態編程語言也在“悄悄”地攻城掠地,占據了相當的開發領域,比如 JavaScript 業已成為 Web 客戶端事實上的主流語言。
最近這幾 年,動態編程語言變得日益流行,比如 Python 、 Ruby 都非常活躍,使用者眾多。
這里有一個問題,為什么我們需要在開發中應用動態編程語言?與 C# 和 Java 這類已經非常成熟且功能強大的靜態類型編程語言相比,動態編程語言有何優勢?
簡單地說,使用動態編程語言開發擁有以下的特性:
(1 )支持REPL ( Read-evaluate-print Loop :“讀入 à 執行 à 輸出”循環迭代)的開發模式,整個過程簡潔明了,直指問題的核心。
舉個簡單的例子, 圖 2 所示為使用 IronPython[2] 編程計算“ 1+2+ …… +100 ”的屏幕截圖,我們可以快速地輸入一段完成累加求和的代碼,然后馬上就可以看到結果:
圖 2 使用 IronPython編程
如果使用 C# 開發就麻煩多了,您得先用 Visual Studio 創建一個項目,然后向其中添加一個類,在類中寫一個方法完成求和的功能,再編寫調用這一方法的代碼,編譯、排錯,最后才能得到所需的結果……
很明顯,對于那些短小的工作任務而言,動態編程語言所具備的這種 REPL 開發模式具有很大的吸引力。
( 2 )擴展方便。用戶可以隨時對代碼進行調整,需要什么功能直接往動態對象上“加”就是了,不要時又可以移除它們。而且這種修改可以馬上生效,并不需要像 C# 那樣必須先修改類型的定義和聲明,編譯之后新方法才可用。
換句話說:使用動態語言編程,不需要“重量級”的 OOAD ,整個開發過程迭代迅速而從不拖泥帶水 。
( 3 )動態編程語言的類型解析是在運行時完成的,可以省去許多不必要的類型轉換代碼,因此,與靜態編程語相比,動態編程語言寫的代碼往往更緊湊,量更少。
動態編程語言主要的弱點有兩個:
( 1 )代碼中的許多錯誤要等到運行時才能發現,而且需要特定的運行環境支持,對其進行測試不太方便,也不支持許多用于提升代碼質量的各種軟件工程工具,因此不太適合于開發規模較大的、包容復雜處理邏輯的應用系統。
( 2 )與靜態編程語言相比,動態編程語言編寫的程序性能較低。不過隨著計算機軟硬件技術的不斷進步,比如多核 CPU 的廣泛應用,動態編程語言引擎和運行環境不斷地優化,動態編程語言編寫的程序性能在不斷地提升,在特定的應用場景下,甚至可以逼近靜態語言編寫的程序。
2 擁抱“動態編程”特性的 C# 4
為了讓 C# 、 Visual Basic 等 .NET 編程語言能具備動態編程語言的特性, .NET 4.0 引入了一個“ DLR ( Dynamic Language Runtime :動態語言運行時) ”( 圖 3 )。
圖 3 DLR:動態語言運行時
DLR 運行于 CLR 之上,提供了一個動態語言的運行環境,從而允許 Python 、 Ruby 等動態語言編寫的程序在 .NET 平臺上運行,同時,現有的 .NET 靜態類型編程語言,比如 C# 和 Visual Basic ,也可以利用 DLR 而擁有一些動態編程語言的特性。
( 1 )使用 C# 4 編寫動態的代碼
C# 4 新增了一個 dynamic 關鍵字,可以用它來編寫“動態”的代碼。
例如,以下代碼創建了一個 ExpandoObject 對象(注意必須定義為 dynamic ):
這一對象的奇特之處在于,我們可以隨時給它增加新成員:
dynamicObj.Increment = new Action(() => dynamicObj.Value++); //添加方法
這些動態添加的成員與普通的類成員用法一樣:
dynamicObj.Increment();//調用方法
Console.WriteLine("dynamicObj.Value={0}",dynamicObj.Value);//訪問字段
對象實現了接口,可看成是一個字典對象,所有動態添加的成員都是這個字典對象中的元素,這意味我們不僅可以添加新成員,還可以隨時移除不再需要的成員:
(dynamicObj as IDictionary<string, object>).Remove("Increment");
方法移除之后,再嘗試訪問此方法將引發RuntimeBinderException異常。
(2)使用dynamic關鍵字簡化與COM組件交互的代碼
要在.NET這個“托管世界”里調用“非托管世界”中的COM組件,我們必須通過“互操作程序集(Interop Assembly)”作為橋梁,“互操作程序集”定義了CLR類型與COM類型之間的對應關系。
只要給.NET項目添加對“互操作程序集”的引用,就可以在.NET應用程序中創建這一程序集所包容的各種類型的實例(即COM包裝器對象),對這些對象的方法調用(或對其屬性的存取)將會被轉發給COM組件。
以調用Word為例,在C# 4.0之前您可能經常需要編寫這樣的代碼:
Object fileName = “MyDoc.docx” ;//指定Word文檔
Object argu = System.Reflection.Missing.Value;
Word.Document doc = wordapp.Documents.Open(ref fileName, ref argu,ref argu, ref argu, ref argu, ref argu, ref argu, ref argu,ref argu, ref argu, ref argu, ref argu, ref argu, ref argu,ref argu, ref argu);
上述對Open()方法的調用語句只能用“恐怖”一詞來形容,其原因是Word組件中的Open()方法定義了太多的參數。
C#4使用dynamic關鍵字,配合從Visual Basic中學來的“命名參數與可選參數”這兩個新語法特性,可以寫出更簡潔的代碼:
dynamic doc = wordapp.Documents.Open(FileName: “MyDoc.docx”);
上述代碼中省去了用不著的參數,并且可以去掉參數前的ref關鍵字。
當上述代碼運行時,DLR會使用反射技術將dynamic表達式“綁定(bind)”到COM互操作程序集中所包容Word.Application代理對象。
(3)C# 4動態編程技術內幕
C#4中所定義的dynamic變量可以引用以下類型的對象:
l 傳統的“靜態”的CLR對象。
l COM包裝器對象。前面已經介紹了這方面的內容。
l 實現了IDynamicMetaObjectProvider接口的“動態對象”,ExpandoObject就是這種類型對象的實例。
l 基于DLR實現的動態語言(比如IronRuby和IronPython)所創建的對象。
從C#程序員角度來看,所有這四種對象都是一樣的,都可用一個dynamic變量引用之,而DLR在程序運行時動態地將方法調用和字段存取請求“綁定”到真正的對象上。
dynamic的功能是由DLR所支撐的,是C#編譯器與DLR分工合作的成果。
請看以下示例代碼:
d++;
C#編譯器在處理上述代碼時,它并不去檢查變量d是否可以支持自增操作,而是為其創建了一個CallSite<T>對象
public static CallSite<Func<CallSite, object, object>> <>p__Site1;
}
中文MSDN將CallSite<T>譯為“動態(調用)站點”,它是DLR中的核心組件之一。
動態站點對象通過CallSite<T>.Create()方法創建, C#編譯器會為其指定一個派生自CallSiteBinder的對象(稱為“動態站點綁定對象”)作為其參數。
動態站點綁定對象是與具體語言相關的,比如IronPython和C#都有各自的動態站點綁定對象。
動態站點綁定對象的主要工作是將代碼中的動態表達式(本例中為d++)轉換為一棵“抽象語法樹(AST:Abstract Syntax Tree)”,這棵語法樹被稱為“DLR Tree”,是在.NET 3.5所引入的LINQ表達式樹的基礎上擴充而來的,因此,有時又稱其為“表達式樹(Expression Tree)”
DLR在內部調用此表達式樹的Compile()方法生成IL指令,得到一個可以被CLR所執行的委托(在本例中其類型就是Func<CallSite, object, object>)。
動態調用站點對象(本例中為<>p__Site1)有一個Target屬性,它負責引用這一生成好的委托。
委托生成之后,動態表達式的執行就體現為委托的執行,其實參由C#編譯器直接“寫死”在IL代碼中。
簡化的代碼示意如下(通過Reflector得到,為便于閱讀,修改了變量名):
object CS$0$0000 = d;
if (<>p__Site1 == null)
<>p__Site1 = CallSite<Func<CallSite, object, bject>>.Create(……);
d = <>p__Site1.Target(<>p__Site1, CS$0$0000);
上述類型推斷、方法綁定及IL代碼生成的工作都是在程序運行時完成的。
(4)動態代碼很慢嗎?
動態編程語言易學易用,代碼緊湊,開發靈活,但性能則一直是它的“軟肋”。為了提升性能,DLR設計了一個三級緩存策略。
動態站點綁定對象會為動態調用表達式轉換而成的語法樹加上相應的測試條件(稱為“test”),構成一個“規則(Rule)”,這個規則可以用于判斷某個語法樹是否可用于特定的動態調用表達式。
舉個例子,請看以下這個動態表達式:
如果在程序運行時d1和d2都是int類型的整數,則DLR生成的規則為:
return (int)d1+(int)d2; //語法樹
“規則”是DLR緩存的主要對象。
前面介紹過的動態站點對象Target屬性所引用的委托是第一級緩存,它實現的處理邏輯是這樣的:
if( d1 is int && d2 is int) //測試條件
return (int)d1+(int)d2; //滿足測試條件,直接返回一個表達式樹
//未命中,則在第2級、第3級緩存中查找,如果找到了,用找到的結果更新第1級緩存
return site.Update(site,d1,d2);
當前版本的DLR第2級緩存了10條規則,第3級則緩存了100條規則。
由于DLR自身設計了一個“規則”緩存系統,又充分利用了CLR所提供的JIT緩存(因為所有動態調用代碼最終都會轉換為CLR可以執行的IL指令,而CLR可以緩存這些代碼),使得動態代碼僅僅在第一次執行時性能較差,后續的連續調用其性能可以逼近靜態代碼。
3 C# 4與動態語言的集成
由于幾乎所有的編程語言都可以使用抽象語法樹來表達,因此,在理論上DLR支持無限多種編程語言間的互操作,在當前版本中,可以實現C#/Visual Basic與IronPython和IronRuby的互操作,相信很快會出現其他動態編程語言的DLR實現。
一個有趣的地方是當前基于DLR實現的動態編程語言都以“Iron”開頭,比如IronRuby和IronPython。IronPython的設計者、DLR的架構設計師Jim Hugunin曾經在微軟PDC 2008大會上解釋說主要是為了避免起一個“Python.NET”或“Python for .NET”之類“微軟味十足”的名字,才有了“IronPython”。他強調:“Iron”系列動態語言將嚴格遵循動態語言自身的標準和規范,尊重這些動態語言已有的歷史和積累,不會引入一些僅限于.NET平臺的新語言特性,并且這些語言的.NET實現保持開源。與此同時,Jim Hugunin指出“Iron”系列語言能很好地與.NET現有類庫、編程語言和工具集成,并且能“嵌入”到.NET宿主程序中。
(1)動態對象通訊協議
由于各種動態編程語言之間的特性相差極大,實現各語言間的互操作是個難題。為此DLR采取了一個聰明的策略,它不去嘗試設計一個“通用的類型系統”(CLR就是這么干的),而是設計了一個“通用的對象通訊協議”,規定所有需要互操作的動態對象必須實現IDynamicMetaObjectProvider接口,此接口定義了一個GetMetaObject()方法,接收一個語法樹對象作為參數,向外界返回一個“動態元數據(DynamicMetaObject)”對象:
DynamicMetaObject對象向外界提供了兩個重要屬性:Restrictions引用一組測試條件,Expression屬性則引用一個語法樹。這兩個屬性組合起來就是可供動態站點對象緩存的“規則(Rule)”。
DLR中的“動態站點綁定對象(CallSiteBinder)”獲取了DynamicMetaObject對象之后,它調用此對象所提供的各個方法創建“規則”,讓“動態站點對象(CallSite<T>)”的Target屬性引用它,完成動態綁定的工作。
(2)動態語言集成環境
為了方便地實現靜態編程語言與各種動態編程語言間的相互集成,DLR提供了一整套稱為“通用寄宿(Common Hosting)”的組件,其中包容ScriptRuntime、ScriptScope等類型。
下面我們以IronPython為例,介紹如何在C# 4開發的程序中集成動態編程語言代碼。
首先需要創建一個ScriptRuntime對象,它是一個最頂層的對象,用于在一個.NET應用程序域中“嵌入”一個特定動態語言的運行環境:
ScriptScope對象類似于C#中的命名空間,其中可以通過定義一些變量向動態代碼傳入數據,比如下述代碼將一個C#創建的ExpandoObject對象傳給Python代碼:
//C#創建動態對象
dynamic expando = new ExpandoObject();
expando.Name = "JinXuLiang"; //動態添加一個字段
//讓IronPython接收C#創建的Expando對象
scope.SetVariable("ExpandoObject", expando);
string pythonCode = "print ExpandoObject.Name";
//IronPython引擎執行Python語句 engine.CreateScriptSourceFromString(pythonCode).Execute(scope);
上述示例代碼是直接執行Python代碼。在實際開發中,更常見的是直接執行Python文件中的代碼,假設有一個Calculator.py文件,其中定義了一個Add函數:
return a+b
則以下C#代碼可以直接執行之:
dynamic pythonFile = pythonRuntime.UseFile("Calculator.py");
Console.WriteLine(pythonFile.Add(100, 200));
這意味著兩點:
(1)我們現在可以將“靜態”和“動態”編程語言組合起來,開發出一些具有高度交互性的應用程序,使用靜態編程語言搭建系統框架,使用動態編程語言實現交互性,這是一個很值得注意的應用領域。
(2)將來會出現一些“靜態”“動態”編程語言同時適用的庫,向實現“無所不在的復用”目標又前進了一步。
Visual Studio 2010為新的.NET編程語言F#提供了專門的項目模板,但沒有為IronPython和IronRuby之類動態語言的開發提供支持,相信隨著動態語言在.NET平臺之上的應用日趨廣泛,后繼版本的Visual Studio會直接支持動態語言的開發。
從C# 1.0~4.0所走過的路,可以很清晰地看到它的發展軌跡,得到這樣的一個結論:
未來的編程語言應該是多范式的,具有高度的可組合性,在一個項目或產品中組合多個編程語言、使用多種編程范式會變得越來越普遍。
我們可以推斷C#的后繼版本將會在此條道路上越走越遠……