跳到主要內容

C# 的Dynamic Object


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# 程式應該以靜態編程的方式為主
但在以下情況 用動態編程來編寫會較為合適
  1. 需要興COM Object 互動
  2. Duck typing
  3. 需要與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 和 它也可以在執行時轉換型別的。
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 的類別來解決此問題較為適合。

DynamicObject

如上述提及,DynamicObject 比起ExpandoObject 能建立出更加大彈性的Dynamic 類別。 而且比起學習使用IDynamicMetaObject  這個Interface簡單得多,。DynamicObejct 是一個抽象類別, 主要有4種不同類別的方法 可讓你override。
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 上 

留言

這個網誌中的熱門文章

如何使用Macbook 進行W-Fi 竊聽及信號分析

OS X 中的Wi-Fi Analyzer 工具 要對無線網絡進行分析及診斷, 首先要在PC中安裝Wi-Fi Analyzer 應用程式。 Wi-Fi Analyzer是一種可以檢察Wi-Fi 信號強弱,找出適合架設Wi-Fi 的Channel , 搜尋附近Wi-Fi 接點及竊聽經Wi-Fi 傳送的網絡封包(這功能要視乎你的網卡是否支援監控模式)的工具。  在網絡上有不同的Wi-Fi Analyzer 供大家下載。 而在OS X 系統中, 其實已預設安裝了Wireless Diagnostics 程式。   在本文中, 會假定讀者對Wi-Fi 的專業名詞(如noise, ssid bssid 等)有基本的認識。 因此, 只會展示Wireless Diagnostics 的操作方法 。  Wireless Diagnostics  Wireless Diagnostics  是一個預設安裝在 OS X 系統的Wi-Fi 分析工具。 用戶可透過它的GUI 介面操作。  在主畫面中,按下option 鍵並點擊在上角的Wi-fi標記會出現Open Wireless Diagnostics。 然後點選它。 點擊工具列中的Window。 並會出現以下選項 如圖 1. Info     點取Info, 會顯示你現在所使用的Wi-fi資訊。 包括它的BSSID,Mac Address,Channel, Network Interface  等等資訊。 如下圖 2. Logs 點選Logs的話, 你可以選取想要收集的網絡介面日誌資訊。 要注意這設定需要重啓電腦才能生效。 並且它是會把收集到的資訊紀錄在電腦中。 因此當你不需要使用時,請記得把它關閉, 以免造成資源浪費。 3.Scan    點選Scan 的話,系統會立即掃描附近的AccessPoint  並列出它們的資料,包括SSID,Mac Address, Noise,RSSI,Channel 等等 資訊。另外系統也會對系統的現時環境作出評估, 列出最合適架設Wi-Fi 的 Channel。 以下圖 圖中的Summary, 就是系統對現在環境的評估。 Total 是系統偵擦到的AccessPoint 數目。 而Best 2.4GHz和Be

C# XML(輸出教學)

C# XML 輸出教學 XML 是其中一種主流的數據儲存格式之一, 在C# 中, 大概有3種不同的寫法把數據輸出成XML 文件。 在本文中, 我會利用一個簡單的Window form 程式介紹這3種不同的寫法。  以下是範例的XML 格式 <Im>    <Number>xx</Number>    <Name> xxx</Name>    <Type> xxx <Type>    <Data> xxx </Data> </Im> 從以上的結構可輕易看得出, 這個範例是把一張圖片輸出成XML 格式。 這是範例中使用的圖片 成功輸出的XML 文件 在Chrome 中顯示 Window form 程式 XML 輸出方法 方法一 首先要介紹的寫法是利用XMLElement 的操作, 在XmlDocument 中構建XML 節點。 上述程式碼先構建一個全新的XmlDocument, 然後逐一構建節點。 最後把構建的節點放到對應的節點或XML Root 節點下。 成功構建後便使用XmlDocument 中的save 方法輸出XML 文件。 以上要注意的事是使用這種方法生成XML 的話, 如遇到要處理圖片的情況。 請先把圖片轉換成base64 字串後, 再放到節點的inner Text 中。 方法二 第二種方法是利用XmlTextWriter 把xml node 按順序寫入Filestream 中。 最後輸出成XML 文件。 程序碼如下, 和方法1相比, 如遇到要處理圖片的情況下, 則只需把圖片的binary data  放到byte 類型的數組中, 再利用XmlTextWriter中的WriteBase64 方法便可。 不用自行轉換成base64字串。 但要留意的事是 由於是使用XmlTextWriter 是把text 以串流的型式輸出。 所以寫入資料流 時最好要按Xml結構的順序寫入。 因為在串流中append 結構是比較困難的事。 方法三 最後一種方法是使用XML.Serialization 空間下的方法, 個人比