第 115 講:C# 4 之動態(tài)綁定(二):DynamicObject 類型
在上一節(jié)內(nèi)容里我們講到了如何去使用一個(gè) dynamic
關(guān)鍵字定義出來的類型,以及它的底層原理(實(shí)現(xiàn)是用的 object
來接收的,使得過程會產(chǎn)生裝拆箱操作)。
今天我們來看看我們前文沒有解決的另外一個(gè)維度的問題,就是如何將一個(gè)對象動態(tài)化,搞成一個(gè)直接在后面用小數(shù)點(diǎn)跟上一個(gè)可能完全不存在的屬性,比如前文的 .FirstName
和 .LastName
這樣的規(guī)則。
Part 1 引例:數(shù)據(jù)字典包裝類
1-1 使用字典類型
考慮一種用法。假設(shè)我定義了一個(gè)字典,這個(gè)字典包含了眾多我們可能在程序里會用到的文字信息。舉個(gè)例子,我們的程序是走國際化路線的,也就意味著我們可能需要實(shí)現(xiàn)一套代碼,存儲中文和英語兩種不同的語言的用法。比如說,我有一個(gè)求和運(yùn)算的簡易程序,有兩個(gè)輸入框和一個(gè)按鈕,按鈕提示用戶按下去就獲取結(jié)果。按鈕在中文環(huán)境下是顯示為“求和”,而在英語里則顯示為“add”。
咱們簡化一下。我們不在這篇文章里實(shí)現(xiàn)整個(gè)代碼,而只關(guān)心這個(gè)存儲的“求和”和“add”這個(gè)信息到代碼里去,并且怎么調(diào)取出來。顯然,我們可以想到一個(gè)辦法:字典。
字典數(shù)據(jù)類型 Dictionary<,>
可以按照指定類型來獲取數(shù)值。而且它在 C# 里有一個(gè)騷操作,使用索引器的語法,就可以取值。于是,考慮這樣的行為,我們可以這么去做。
這里用到一個(gè) C# 提供的字典的初始化器的語法。字典的元素初始化器語法的最外層和別的數(shù)據(jù)類型也一樣,也是一對大括號。不過和別的類型不一樣的地方是,因?yàn)樽值涞?
Add
方法要兩個(gè)元素,因此 C# 對這個(gè)數(shù)據(jù)類型具有特殊的初始化語法規(guī)則:使用的是嵌套級別的一對大括號,大括號只包含兩個(gè)元素,其中第一個(gè)是它的鍵,而第二個(gè)則是它的值。這個(gè)語法在前文 C# 3 里并未說明,因?yàn)樵谥暗奈恼吕镉貌坏皆摂?shù)據(jù)類型,所以就沒有過多講解。
1-2 封裝一個(gè) ResourceDictionary
類型
為了我們稍后能夠靈活穩(wěn)定處理多個(gè)語言的話,我們可以包裝一個(gè)專門的類型:ResourceDictionary
類型:
比如這樣。很好理解,對吧。
當(dāng)然,你用
sealed class
也可以,這里用struct
單純是因?yàn)樗鼉?nèi)部只有一個(gè)數(shù)據(jù)成員_kvp
,還是引用類型的。學(xué)過 C# 的小伙伴應(yīng)該都知道,嵌套級別的數(shù)據(jù)成員,如果是引用類型的話,不管是存儲在值類型里,還是在引用類型里,它都是以引用的形式存在。所謂的引用,在底層也就是一個(gè)一個(gè)的地址數(shù)據(jù)。所以,這里的_kvp
其實(shí)是一個(gè)地址罷了。一個(gè)地址數(shù)值只占據(jù)很小的內(nèi)存空間,因此定義為struct
的話,在實(shí)例化該類型的對象的時(shí)候,比class
就要快上不少。
接著,我們有了這個(gè)類型之后,將對象給存儲進(jìn)去。
1-3 CultureInfo.CurrentCulture
屬性獲取語言習(xí)慣
那么,如何去做到按語言去取字典的數(shù)值呢?這里我們用到的是一個(gè)叫國際化的類型:CultureInfo
。這個(gè)類型可以取出我們當(dāng)前用戶在運(yùn)行的這個(gè)程序所在操作系統(tǒng),用的是什么語言。
這里稍微說清楚一點(diǎn)。由于地理位置的特殊性,臺灣地區(qū)、香港地區(qū)和澳門地區(qū)作為使用繁體字的地區(qū),它們的習(xí)慣和內(nèi)陸地區(qū)使用的中文并不相同,內(nèi)陸使用的是簡體字,而臺灣、澳門和香港地區(qū)則用的是繁體字。而且就臺灣、澳門和香港這三個(gè)地區(qū)來看,它們自己的繁體字寫法也有時(shí)候不相同,因此如果你在臺灣地區(qū)、香港地區(qū)的話,得到的結(jié)果就不會是 zh-CN
這個(gè)結(jié)果,比如臺灣地區(qū)在這個(gè)規(guī)則上得到的結(jié)果是 zh-TW
而不是 zh-CN
。雖然咱們都知道,它們是中國不可分割的一部分,但對于文字習(xí)慣來說,使用中文漢字的習(xí)慣并不相同,因此就導(dǎo)致了同樣使用中文的地方也有不同的使用規(guī)范和標(biāo)準(zhǔn),因此,臺灣、香港和澳門地區(qū)在語言的使用標(biāo)準(zhǔn)上就被分離出來了。所以請不要對于這樣的問題進(jìn)行雞蛋里挑骨頭的無理取鬧。
1-4 封裝一個(gè) Resource
類型作為終極類型
那么,我們現(xiàn)在有了包裝類型,并且也學(xué)會了如何去按語言分配和取字典信息了,下面我們就來完善整個(gè)行為。我們要想讓用戶可以優(yōu)雅地使用這個(gè)多語言的字典,我們光實(shí)例化出來兩個(gè) ResourceDictionary
還不夠。因?yàn)檫@樣并不方便,像是做特別復(fù)雜的國際化處理的時(shí)候,這樣的字典可能包含幾十個(gè)。是的,確實(shí)是幾十個(gè)。所以這樣實(shí)例化字典之后,使用 switch
來判斷使用具體哪個(gè)字典的行為并不優(yōu)雅。那么怎么做呢?
我們再套一層 Dictionary<,>
對象。這次,我們把鍵值對的鍵和值分別對應(yīng)上 string
和 ResourceDictionary
。其中的 string
是這里通過 CultureInfo.CurrentCulture
屬性得到的實(shí)例,取 Name
而得到的字符串結(jié)果;而通過字符串相等性的比較,我們就可以達(dá)到找尋對應(yīng)字典的用法。
Resource
類型來存儲它們。通過我們添加的 Add
1-5 追加 Resource
的字典取值成員
既然有了終極類型了,那么我們自然是需要通過這個(gè)綜合對象來完成不同語言取不同字典的規(guī)則。對于這個(gè)類型來說,我們需要開放一個(gè)用戶可以使用的取值規(guī)則。做法是開一個(gè)和 ResourceDictionary
一樣用途的索引器,傳入 string
類型當(dāng)參數(shù)。由于 ResourceDictionary
的索引器是因?yàn)榈讓拥淖值浔话b起來了所以必須打開索引器來取里面的數(shù)據(jù),而 Resource
類型是高階的用法,所以它的索引器除了取值用,還要額外帶有判斷語言習(xí)慣的操作。所以,我們要設(shè)計(jì)為這樣才比較合理:
1-6 試著使用一下自己寫的數(shù)據(jù)字典包裝類
然后,我們就算完成了整個(gè)封裝包裝過程。下面,我們只需要在主方法里進(jìn)行處即可。
resource
變量的索引器操作。這個(gè) resource
就是前文里定義出來的那個(gè) resource
來看看效果:

“求和”二字就是我們最開始在主方法里寫的讀入的數(shù)據(jù)信息“求和”。至此,引例就完成了。如果你還需要寫更多的語言的字典的話,只需要往 resource
變量里加就行了。而至于這個(gè) zh-CN
啊,en-US
怎么得來的,請參考網(wǎng)上給出的表格。這個(gè)叫做 (英語叫 Locale Codes)。
說起來很有意思,i18n 這個(gè)詞語的由來竟然是因?yàn)?internationalization 單詞太長了,一共 20 個(gè)字母,i 和 n 是它的頭尾,剩下一共 18 個(gè)字母,所以就簡寫成 i18n 了。
Part 2 DynamicObject
類型
在我們前文的取值代碼里,我們用到了索引器:
resource["鍵"]
我們想讓它也可以生效。這怎么做到呢?難道直接把前面的 var resource = ...
給改成 dynamic resource = ...
這么簡單嗎?當(dāng)然不是。因?yàn)檫@里的索引器參數(shù)是一個(gè)字符串啊,我們直接是將這個(gè)字符串同等當(dāng)成屬性方在了實(shí)例的右邊。這種用法實(shí)在是不可能只是簡單改成 dynamic
而已。那么,怎么做呢?
2-1 定義單例
單例模式(Singleton Pattern)不知道你聽說過沒有。這是 C# 編程語言里的一種設(shè)計(jì)模式。可能你對設(shè)計(jì)模式并不熟悉,所以我們就直接在這里說清楚了吧。單例模式就是往這個(gè)類型里創(chuàng)建一個(gè)可以隨時(shí)隨地訪問的靜態(tài)只讀實(shí)例,并防止用戶再次對類型進(jìn)行實(shí)例化的設(shè)計(jì)模式。做法很簡單,改掉兩處代碼:
定義單例;
私有化構(gòu)造器。
私有化構(gòu)造器,然后把構(gòu)造器呢,用于這個(gè)靜態(tài)只讀的實(shí)例 Instance
,它作為唯一一個(gè)你可以對接使用 Resource
類型對象的方案而存在。這就叫單例模式。
單例模式的其中一個(gè)作用(即我們這里用到的作用)是防止用戶誤用代碼,創(chuàng)建過多沒有意義的實(shí)例出來。
2-2 讓 Resource
類型從 DynamicObject
派生
接下來就是這篇文章的重頭戲了。我們需要讓 Resource
類型從 DynamicObject
類型派生。好在我們剛好給 Resource
用引用類型實(shí)現(xiàn)了。因?yàn)橹殿愋褪菬o法自定義繼承派生關(guān)系的,所以這個(gè)只能引用類型來做。
把頭部改成這樣即可。
這個(gè) DynamicObject
作為關(guān)鍵的一步,我先說明一下它是什么。
它是我們將對象動態(tài)化的一個(gè)重要規(guī)則和約定。如果一個(gè)對象可以使用前文那樣的動態(tài)屬性調(diào)用一個(gè)完全沒有的成員,卻可以定義出一套固定的取值規(guī)則和標(biāo)準(zhǔn)的話,那么必須要從這個(gè) DynamicObject
類型進(jìn)行派生。派生之后,對象就有了 dynamic
后引用一個(gè)完全不存在的成員,但又可以正確執(zhí)行的能力了。
2-3 重寫 TryGetMember
方法
接著,雖然我們并未對代碼進(jìn)行重寫好像也沒問題,但是我們這里需要重寫一個(gè)里面的方法,目的是為了關(guān)聯(lián)上前文的 resource.AddOperationName
的神奇調(diào)用規(guī)則,以及字典。
在從 DynamicObject
類型派生之后,我們可以搞到這樣的重寫:
輸入 override
,就可以看到 TryGetMember
方法,然后按 tab 按鍵確認(rèn)你的輸入,就可以得到這樣的代碼。不過,這個(gè) base.TryGetMember
是在調(diào)用基類型的默認(rèn)的代碼,因此沒有意義,我們要將其刪除,改成我們自己用的代碼。
先來簡要介紹一下兩個(gè)參數(shù)。第一個(gè)參數(shù) binder
提供綁定的信息,也就是我們剛才使用的 resource.AddOperationName
語法里,屬性引用的這個(gè) AddOperationName
的基本用法規(guī)則和信息;而第二個(gè)參數(shù),則是返回我們應(yīng)該正常通過 resource.AddOperationName
的用法應(yīng)該正常返回什么結(jié)果出來。返回值 bool
類型是暗示了我們是否可以這么去使用。
考慮到例子里,我們是按字典取值的,所以我們可以把返回值定義理解為“是否可以成功從字典里取到合適的結(jié)果”,而 out object result
這個(gè)參數(shù)則返回出來這個(gè)看起來很像是屬性引用的表達(dá)式的結(jié)果。
我們改寫代碼為這樣:
看起來實(shí)現(xiàn)有些復(fù)雜,但多數(shù)都是注釋文字。我們仔細(xì)理解一下注釋。
首先,我們通過第一個(gè)參數(shù) binder
的 Name
屬性,獲取到屬性引用的那個(gè)成員的信息。因?yàn)檫@里是動態(tài)使用的,編譯器并不會發(fā)現(xiàn)錯(cuò)誤,所以編譯通過;而對于底層來說,這個(gè)屬性引用實(shí)際上是不存在的,所以它不外乎就是一個(gè)字符串一樣的存在。因此,binder.Name
得到的結(jié)果就是字符串形式的表達(dá)。這一點(diǎn)很神奇,屬性引用的屬性居然被轉(zhuǎn)為了字符串寫法。
然后,我們使用一個(gè) try
-catch
語句來完成取值。注意,這里盡量不要拋異常,是為了保證取值的成功或失敗。失敗的話,C# 會自動產(chǎn)生異常,因此不必我們手寫。懂意思嗎?就是說,這個(gè)方法只是動態(tài)調(diào)用過程的其中核心一步,而其它的步驟都被 C# 包裝好了,行為也都包裝好了,因此不必我們在方法里拋異常來告知用戶出錯(cuò),拋異常留給 C# 就行。這個(gè)方法如果返回 false
,就表示我們綁定失敗,于是就會產(chǎn)生 RuntimeBinderException
異常,在前文已經(jīng)說過了。
至此,我們就算把關(guān)鍵的行為和操作給說完了。
2-4 單例模式的類型改成 dynamic
這一步很關(guān)鍵。我們在前文給出的是 Resource Instance = new Resource()
,現(xiàn)在我們需要改成 dynamic Instance = new Resource()
:
這一步很關(guān)鍵。目的是為了讓對象真的可以動態(tài)處理。因?yàn)槲覀冎暗?Resource
類型都是以實(shí)際類型存儲和存在的,這里改成 dynamic
后,對象又從 DynamicObject
類型派生,因此 C# 會識別代碼,并自動知道如何去處理。
2-5 修改 Main
方法的代碼
最后一步是修改 Main
的代碼。我們改下后面的部分,完整的代碼如下:
這里的 Resource.Instance
就是在使用這個(gè)單例,而這里的 Add
方法和 AddOperationName
這個(gè)“假屬性”也都在正常操作和調(diào)用方法的具體內(nèi)容。其中,Add
方法本身就存在,所以不管它是不是 dynamic
類型的,這個(gè)方法也都會被正常處理和執(zhí)行到;而下面的 AddOperationName
并不存在,但屬性的引用的底層實(shí)現(xiàn)被我們重寫了,所以也不會出錯(cuò)。
再來看下這次我們修改后的完整版代碼吧:
來看這次我們得到的結(jié)果:

非常好,我們修改了之后也得到了我們要的結(jié)果,而且代碼雖然更多了,但是主方法里的代碼更優(yōu)雅了。封裝是麻煩的,但是用起來是方便的,這就是編程。
Part 3 DynamicObject
支持的處理行為
實(shí)際上,不僅是前文用到的屬性的綁定,還有很多的成員全都可以在 DynamicObject
的部分成員重寫之后完成正常的執(zhí)行和使用。我們打開 DynamicObject
對象的元數(shù)據(jù):

可以發(fā)現(xiàn)它竟然有這么多方法。其中:
TryBinaryOperation
:對一個(gè)dynamic
對象參與的二元運(yùn)算符行為進(jìn)行執(zhí)行處理。比如說我對了一個(gè)dynamic
對象和一個(gè)int
使用了+
運(yùn)算符操作,那么具體在底層綁定和定義處理規(guī)則的時(shí)候,你需要重寫這個(gè)方法;TryConvert
:對一個(gè)dynamic
對象上使用到的強(qiáng)制轉(zhuǎn)換或隱式轉(zhuǎn)換(雖然dynamic
類型自身很少有隱式轉(zhuǎn)換)進(jìn)行處理。比如如果我用到了一個(gè)dynamic
類型的obj
對象的(int)obj
操作,你就需要重寫這個(gè)方法;TryCreateInstance
:對一個(gè)dynamic
對象進(jìn)行實(shí)例化。常見的情況就是new
一個(gè)出來;TryDeleteIndex
:這個(gè)方法對應(yīng)到 C# 里沒有相同的語法,所以就不介紹了;TryDeleteMember
:這個(gè)方法對應(yīng)到 C# 里也沒有相同的語法,所以就不介紹了;TryGetIndex
:對一個(gè)dynamic
對象使用索引器取值的時(shí)候。比如dynamic
的對象obj
使用了obj[1, 2, 3]
如此的語法;TryGetMember
:對一個(gè)dynamic
對象使用了屬性或字段的時(shí)候。這個(gè)方法就是前文里重寫的那個(gè);TryInvoke
:對一個(gè)dynamic
對象,直接當(dāng)委托的形式直接跟小括號調(diào)用的語法。比如dynamic
對象obj
直接使用obj(1, 2, 3)
這樣的語法的時(shí)候;TryInvokeMember
:對一個(gè)dynamic
對象調(diào)用類似方法調(diào)用的規(guī)則。比如dynamic
對象obj
調(diào)用類似obj.InnerMethod(1, 3)
這樣的方法的時(shí)候;TrySetIndex
:對一個(gè)dynamic
對象使用索引器在賦值的時(shí)候。比如dynamic
對象obj
使用了obj[3] = 0
如此的語法。注意它和前文TryGetIndex
的區(qū)別,前文是取值,這里是賦值;TrySetMember
:對dynamic
對象使用字段或?qū)傩酝镔x值的時(shí)候;TryUnaryOperation
:對dynamic
對象使用一元運(yùn)算符的時(shí)候。比如對一個(gè)dynamic
對象使用了自增運(yùn)算符++
的時(shí)候。
至此,我們就把 C# 提供的兩種動態(tài)類型的語法給大家介紹完了。下一講的內(nèi)容則繼續(xù)是 C# 4 的語法:用于委托和接口里的泛型參數(shù)的協(xié)變性和逆變性。這個(gè)規(guī)則好像在 C# 2 提過,但它是針對委托的參數(shù)和返回值的,并沒有說泛型參數(shù)。泛型參數(shù)有一個(gè)比較重要的轉(zhuǎn)換優(yōu)化,在 C# 4 里才有,這個(gè)我們在下一講會給大家介紹。