C# 的Dynamic Object
介紹
在.Net 4.0 版本, C# 引入了動態編程。 實則上它並不是C#
語言的一部分, 因為它是通過引入Dynamic Language Runtime(DLR library) 來實現的。 對DLR Library 有興趣的話 可瀏覽(https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/dynamic-language-runtime-overview)
但對於編譯語言(Compiled Language) 來說, 引入Dynamic 特性在當時(.Net 4.0 是在2010 發佈的)是一種大躍進。
雖然C# 中已有支援動態特性, 但C# 始終是一種編譯語言 。所以我還是建議如非必要的話,還是不要使用動態編程。 如果靜態編譯是可以做到這個效果的話就用靜態編譯實現。 因為使用動態編程的話, 在編譯時是不會提示你的程式碼會不會有錯誤, 因此大大增加了執行時出現錯誤的風險。
而且由於動態編程是late-binding. 所以物件的類別在執行時才會決定。這種做法相對於靜態編譯是會有執行效率低的問題。 因此 編寫C# 程式應該以靜態編程的方式為主。
但在以下情況, 用動態編程來編寫會較為合適
- 需要興COM Object 互動
- Duck typing
- 需要與dynamic script language 如IronPython 或Ironruby 等互動。
以上的情況, 有興趣的讀者可從互聯網中參考, 本文的以下部分會集中說明C# 中 動態編程的基本語法, 介紹ExpandObject, DynamicObject 和 IDynamicMetaObject 及ExpandoObject 和DynamicObject 的用法。
Dynamic Keyword
若要在C# 中使用dynamic object, dynamic 這個keyword 你是絕對需要知道的。 當一個變量以dynamic 作為它的型態。 便代表它是late-binding. 編譯器在執行編譯時不會判斷它的型態。 因此, 以下的code 會顯示
由於使用dynamic 作為變量a 的類別, 變量a 可以在程式執行間期分別賦與不同類別的值。由此可見, C#中 dynamic keyword 是DLR 編程的基礎。
當然, dynamic keyword 除了可用在局部變量外, 函數的回傳值、函數的參數和類別中的成員變量也是可以使用dynamic keyword 來定義的。 以下的定義在C#中是合法的。
當然, 在應用軟件開發的工作中定義這樣一個類別是不可能的, 因為這樣的代碼會對日後的代碼更改工作洐生很大問題。 在這只是展示類別的成員也可以定義成Dynamic 類別。
以下以DynamicScopeClass 作為例子展示Dynamic 成員的特性。
和普通的dynamic 局部變量一樣, 類別成員不管時實例成員或是類別成員,也可以使用dynamic keyword來實現late-binding 和 它也可以在執行時轉換型別的。
以下以DynamicScopeClass 作為例子展示Dynamic 成員的特性。
和普通的dynamic 局部變量一樣, 類別成員不管時實例成員或是類別成員,也可以使用dynamic keyword來實現late-binding 和 它也可以在執行時轉換型別的。
Dynamic Keyword 的概念相對以下部分是沒比較易懂的, 只需要知道它是late-binding, 它的任何方法及屬性的調用語句編譯器在編譯時並不會執行檢查及它的可用Scope 便可。
ExpandoObject, DynamicObject and IDynamicObject
如果你有用過script type language 開發程式的話, 這樣的寫法你應該並不陌生。
以上是Javascript 的程式碼
這段代碼建立了一個Object 的實例, 並且這個實例一開始是沒有一個名為x 的成員的。 當調用程式碼a.x = 10 時, 動態語言會檢查實例中有沒有一個名為x 的成員, 有的話就更新它的值, 沒有的話就建立這個成員並且賦值給它。
Script language 能動態定義物件的屬性, 它並不需要像C# 一樣在類別的定義中確實定義該屬性的型別。 而是通過調用的時候建立及賦值。
那麼在C# 中, 也可以以這種風格來編程嗎?
答案是可以的!
建立的方法是透過建立ExpandoObject, 繼承DynamicObject 或實作IDynamicMetaObject. 它們之間的分別是
Expandoobject 是簡單的動態物件, 你可以透過建立它的實例來實現上述的動態編程
DynamicObject 相比起ExpandoObject, 除了可以實現動態屬性外, 還可以通過override 來定義新的類別特性。
IDynamicMetaObject 是實現Dynamic object 編程的核心, 你可以通過實現這個interface 來設計一個完全獨立的dynamic object。但在此我並不建議你這樣做,
第一點是實IDynamicMetaObject 相對前兩種方法復雜。 第二點是C# 團隊在實現DLR Library 時已作出很多的執行效能的考量。 因此自己實現的DynamicObject 並不一定比類別庫中的DynamicObject 好。
ExpandoObject
使用ExpandoObject,
你只需要建立它的實例便可。
如上圖所示, 你可以不斷為它增加屬性或方法,
但由於ExpandoObject 並沒有任何帶有參數的建構式, 所以在初始化它時並不方便。
舉例如果我們有個Json 的字串
"{\"person\": { \"name\": \"tester\", \"age\": 20, \"saving\": 100.6, \"creditCard\": [101, 102,301], \"valid\": true}, \"movies\": [{\"name\": \"Star War 8\", \"year\" :2017}, {\"name\": \"The shape of water\", \"year\": \"2017\"}]}"
如果使用ExpandoObject 來構建一個包含這些資料的JSON Object, 做法只能夠使用JSON Library 如Netwonsoft JSON, 先把它先變成JSON Object 之後 再逐一賦值。 此時, ExpandoObject 的簡單方法已不能解決你的需要。 建立一個繼承DynamicObject 的類別來解決此問題較為適合。
但由於ExpandoObject 並沒有任何帶有參數的建構式, 所以在初始化它時並不方便。
舉例如果我們有個Json 的字串
"{\"person\": { \"name\": \"tester\", \"age\": 20, \"saving\": 100.6, \"creditCard\": [101, 102,301], \"valid\": true}, \"movies\": [{\"name\": \"Star War 8\", \"year\" :2017}, {\"name\": \"The shape of water\", \"year\": \"2017\"}]}"
如果使用ExpandoObject 來構建一個包含這些資料的JSON Object, 做法只能夠使用JSON Library 如Netwonsoft JSON, 先把它先變成JSON Object 之後 再逐一賦值。 此時, ExpandoObject 的簡單方法已不能解決你的需要。 建立一個繼承DynamicObject 的類別來解決此問題較為適合。
DynamicObject
1. TryXXX() 方法: 動態地建立或讀取類別的成員。
2. Equals, ToString(), GetHashCode: 一般抽象類別可Override的方法。
3. GetDynamicMemberNames(): 返回類別現在擁有的成員變數。
4. GetMetaObject(): 返回DLR 用到的MetaObject.。
以下例子會讀取上述中的JsonString 來建立一個dynamic object. 並且能動態地呼叫它的成員變數, 如 jsonObj.person.name 便會顯示"tester", jsonObj.movies[0] 會顯示 {"name": "Star War 8", "year": 2017" }。
主要的實現方式是通過override Tryxxx() 的方法。
代碼如下
DynamicJsonObject Example 的輸出結果如下
實作撮要
DynamicJsonObject 在類別內定義一個名為node 的JObject 成員和一個名為arr的JArray成員, 並且透過一個靜態方法parseJsonString parsing JSON 字串並把一個dynamic 類別的DynamicJsonObject 回傳給用戶端。 之於 Override ToString 方法的目的是因為當遇上一個需要printout 的節點的類別是JObject 或者是JArray 時, 它的詳細內容也可以輸出。 如上圖中的Person。 為了令到DynamicJsonObject能夠使用jsonObj.person.name 或jsonObj.movies[0] 這兩種形式來存取資料。 在DynamicJsonObject 中必需override TryGetMember 和TryGetIndex 2個方法。TryGetMember
和一般的TryXXX() Method 一樣, 它的回傳值是bool, 而獲得的資料則以out的形式傳出用戶端。 首先先看看代碼中的 binder.name , 它就是你所存取成員的名稱。 如jsonObj.person 的話, binder.name 就是person 了。
所以首先要做的事就是檢查json node中是否有這個節點。 如果json node中沒有這個節點的話, 則呼叫底層的TryGetMember便可, 它會幫你回傳false 和拋出RuntimeBinderException例外。
第2件要做的事是判斷 節點中的值是JValue(普通型別 如int, string and etc), JObject(其它類別), 或JArray(數組)。 因為不同的型別傳出的out變數是不同的。 最後記得拿取資料後要回傳true值, 否則雖然獲得了資料, 但它依然會拋出RuntimeBinderException例外。
TryGetIndex
TryGetIndex 方法的實現原理和TryGetMember差不多。 在此我不再重覆多說。 值得注意的一點是TryGetIndex中多了一個object indexes[]參數。 如用戶端調用jsonObj.movies[1]。 則 indexes[0] 中的值就是 1。 即indexes 的第一個元素是用戶端想拿取的index。
總結
本文上敍述了C# 的動態編程特性, 介紹了dynamic 類別的用法和在變量上的可用範圍 ,ExpandoObject,DynamicObject 及 IDynamicMetaObject 三種之簡的基本分別。 並以普通的編程例子來講解ExpandoObject 和DynamicObject 的用法。 雖然現階段來說, 在C#中使用動態編程還是少至有少。 以及基於代碼的維護性及效能作為考量, 靜態編程依然是C#的主流。
但學習C#的動態編程 還是有一定的價值。 特別是當你需要處理本文中介紹部分中提及的情況。
本文的代碼已放在Github 上
留言
張貼留言