第 67 講:C# 2 之泛型(一):泛型的基本使用
歡迎大家來(lái)到 C# 的新語(yǔ)法板塊。C# 的新語(yǔ)法非常多,多到三言兩語(yǔ)無(wú)法說(shuō)完。我大概數(shù)了一下,才到 C# 8,新語(yǔ)法就已經(jīng)超過(guò)一百個(gè)了。大家可以參考獲取所有的新語(yǔ)法。我們將會(huì)按照順序給大家講解 C# 的這些新語(yǔ)法。
今天第一個(gè)要介紹的語(yǔ)法是泛型。C# 2 的泛型非常復(fù)雜,所以我們一兩天都說(shuō)不完,所以以后類似這種復(fù)雜語(yǔ)法,我會(huì)考慮分為多講來(lái)給大家介紹。
Part 1 為什么得有泛型?
還記得我們之前學(xué)過(guò)的一些方法嗎?有一些方法我們會(huì)要求傳入一個(gè) object
類型的參數(shù)作為接收,這樣是為了兼容所有非指針類型的數(shù)據(jù)均可以往里面?zhèn)魅肴缓髤⑴c計(jì)算的過(guò)程,這樣的好處就是一勞永逸。因?yàn)槲矣辛?object
作為接收方,那么你不管是 int
也好,string
也好,什么數(shù)據(jù)類型均可以往里面扔。比如 ArrayList
,我們可以接收任何數(shù)據(jù)類型作為每一個(gè)元素,這樣就非常靈活。如果我通過(guò) C# 基本語(yǔ)法來(lái)實(shí)現(xiàn) ArrayList
但不用 object
接收的話,那么整個(gè)數(shù)據(jù)類型就只能接收一個(gè)固定數(shù)據(jù)類型的成員了。首先,值類型沒(méi)有自定義的繼承機(jī)制,這樣的話,我如果能傳入 int
,那么 double
、char
這些類型的元素就不能使用 ArrayList
這個(gè)數(shù)據(jù)類型,因?yàn)?ArrayList
只能傳入 int
。再比如之前學(xué)到的多線程功能。多線程里我們有 object state
作為接收。但凡這個(gè)參數(shù)是一個(gè)具體數(shù)據(jù)類型,那么別的數(shù)據(jù)類型都無(wú)法傳入,導(dǎo)致方法不夠靈活。這個(gè)就是為什么 C# 會(huì)使用“object
是所有類型的基類型”這個(gè)機(jī)制來(lái)做很多事情的原因。
再來(lái)說(shuō)第二個(gè)問(wèn)題。object
是所有非指針類型的基類型,而在處理的時(shí)候我們不只是使用到引用類型的繼承機(jī)制,有些時(shí)候我們也喜歡傳入一個(gè) int
、double
等值類型的數(shù)據(jù)作為數(shù)據(jù)的處理過(guò)程。如果我們使用 object
進(jìn)行接收,那么必然會(huì)導(dǎo)致裝箱操作,影響到底層的處理性能。雖然裝箱對(duì)于實(shí)際運(yùn)行程序的過(guò)程沒(méi)有啥大的影響,畢竟結(jié)果也都是正確的,但裝箱不可避免的弊端是影響性能這一個(gè)問(wèn)題:它會(huì)在堆內(nèi)存里創(chuàng)建開辟內(nèi)存空間存儲(chǔ)值類型數(shù)據(jù)。這是不必要的。我們只想優(yōu)雅地處理數(shù)據(jù)而已,為什么沒(méi)有合適的操作能夠解決這一點(diǎn)呢?
那么,為了通用性的同時(shí)也能夠避免裝箱,所以 C# 2 誕生了一個(gè)新語(yǔ)法,也是貫穿 C# 靈魂的新特性:泛型(Generic)。泛型既可以解決裝箱的問(wèn)題,也可以盡可能大地表達(dá)出通用性這個(gè)理念。不過(guò)這里稍微多說(shuō)一句。有很多小伙伴以為這個(gè)泛型的地位很高,所以還以為是 C# 原生語(yǔ)法。實(shí)際上不是的。C# 在第 2 版才開始有泛型。
Part 2 泛型的語(yǔ)法
為了介紹 C# 新的語(yǔ)法,我們先從新的 API:List<T>
類型切入進(jìn)來(lái),方便了解基本語(yǔ)法,然后再來(lái)學(xué)習(xí)如何自己實(shí)現(xiàn)一個(gè)泛型類型。
2-1 基本使用例子
C# 帶有一個(gè)新的 API 叫做 List<T>
類型。注意這個(gè)類型是一個(gè)引用類型,并且語(yǔ)法上有點(diǎn)特殊:類型名后帶一個(gè)尖括號(hào)。注意這個(gè)叫尖括號(hào)而不是小于大于號(hào),是因?yàn)樗谶@里的使用意義不是一個(gè)比較運(yùn)算,而是一對(duì)獨(dú)立于小括號(hào)、中括號(hào)、大括號(hào)的第四種括號(hào)類型。尖括號(hào)在目前 C# 的語(yǔ)法里一般提供給泛型這一個(gè)特性使用,也就是說(shuō),這就是我們的泛型。
我們把 List<T>
的 List
叫做數(shù)據(jù)類型的非泛型部分,把 <T>
叫做泛型部分,整個(gè) List<T>
則為一個(gè)數(shù)據(jù)類型。由于它使用了泛型這個(gè)新語(yǔ)法,因此類似 List<T>
這樣使用了泛型語(yǔ)法的類型也稱為泛型數(shù)據(jù)類型(Generic Type)。另外,在尖括號(hào)里寫的這個(gè)字母 T
,我們叫做泛型參數(shù)(Type Parameter)。參數(shù)?是的,就是我們之前學(xué)習(xí)方法傳參的那個(gè)參數(shù)。這里這個(gè)取名之所以和之前的參數(shù)用的是一樣的名字,原因在于,泛型的真正用法是拿一個(gè)泛型參數(shù)用于代替其中任何一種數(shù)據(jù)類型。它相當(dāng)于你玩撲克牌的替用牌(“癩子”牌),它本身沒(méi)有意義(比如撲克牌往往會(huì)用上廣告牌來(lái)當(dāng)作替用牌),但配合一些固定的出牌模式,比如順子啊、連對(duì)什么的,它就會(huì)自動(dòng)替換上去構(gòu)成這個(gè)模式;而泛型參數(shù)是一個(gè)意思:它自己用一個(gè)隨便自己取的一個(gè)名字占位充當(dāng)一個(gè)“符號(hào)”(是的,名字可以隨便取,按照標(biāo)識(shí)符規(guī)則來(lái)就行,只不過(guò)我們一般習(xí)慣寫成大寫字母 T,因?yàn)?T 是 type 的首字母,暗示了這個(gè)地方應(yīng)該代替掉一個(gè)實(shí)際的數(shù)據(jù)類型;而具體的數(shù)據(jù)類型按照習(xí)慣都是按帕斯卡命名法來(lái)的,所以這個(gè)地方我們用的是大寫字母 T 而不是小寫),而具體在使用的時(shí)候,我們往這個(gè)泛型參數(shù)上放數(shù)據(jù)類型就可以了。
List<T>
這個(gè)類型的誕生,是為了替代原有的 ArrayList
類型,這也就是說(shuō),ArrayList
和 List<T>
的用法一致(或基本一致),但 List<T>
類型從性能、使用等等標(biāo)準(zhǔn)上都比 ArrayList
更好。原來(lái)的 ArrayList
的用法是實(shí)例化、然后 Add
方法往里面加?xùn)|西,最后用一個(gè) foreach
循環(huán)獲取里面存儲(chǔ)的元素。而 List<T>
是一樣的操作,只不過(guò):
注意第 1 行代碼。我們使用 List<int>
語(yǔ)法來(lái)實(shí)例化一個(gè) List<T>
泛型類型集合。這里要注意這個(gè)語(yǔ)法。我們使用 List<int>
想要實(shí)例化一個(gè) List<T>
集合,并且這個(gè)集合里存儲(chǔ)的每一個(gè)元素都是 int
類型。我們嘗試把 int
作為實(shí)際的類型傳入到 List<T>
里替換掉原來(lái)的 T
泛型參數(shù)。這樣做的目的是告知這個(gè)集合,我現(xiàn)在已經(jīng)固定下來(lái)集合到底存儲(chǔ)什么數(shù)據(jù)類型的元素。
泛型就是這么一個(gè)存在,它代表一個(gè)類型,在使用的時(shí)候固定下來(lái)一個(gè)具體的數(shù)據(jù)類型,作為泛型參數(shù)使用,這樣可以使得我在使用的任何時(shí)候,都使用這個(gè)實(shí)際類型來(lái)替換原來(lái)模式化的“替用牌”——這個(gè)泛型參數(shù) T
。
那么,具體這個(gè) List<int>
的 int
我體現(xiàn)在代碼的哪里了呢?第 2、3、4 行代碼以及第 6 行代碼里。第 2、3、4 行代碼的 Add
方法要求我們傳入一個(gè) T
類型的數(shù)值進(jìn)去。但由于我們用 int
代替了 T
,因此這個(gè) T
實(shí)際上就在我實(shí)例化 list
對(duì)象后,固定下來(lái)了。那么 Add
方法也就對(duì)應(yīng)了傳入 int
作為元素添加到集合的末尾,這么一個(gè)行為。正是因?yàn)槲乙髠魅?int
類型,因此第 4 行代碼由于傳入了 string
類型的實(shí)例,因此編譯器會(huì)告訴你,我現(xiàn)在 list
是一個(gè) int
為元素的集合,所以不允許你添加一個(gè) string
實(shí)例進(jìn)去,所以第 4 行代碼是錯(cuò)誤使用。
接著,第 6 行代碼里,我們使用 list
來(lái)完成和 ArrayList
類型一致的迭代行為。不過(guò)由于我們現(xiàn)在確定下來(lái)了具體的元素類型是 int
,因此我們可以大大方方地把這個(gè) int
寫在 element
變量名稱的左邊了。大概都猜得到,List<T>
的每一個(gè)元素是 T
類型,所以 foreach
循環(huán)迭代出來(lái)的每一個(gè)實(shí)例都得是 T
類型。而現(xiàn)在我們 list
已經(jīng)給定元素的數(shù)據(jù)類型,所以我們可以確定和推斷出每一個(gè)集合元素都是 int
類型,所以 foreach
循環(huán)的每一個(gè)元素也都是 int
類型。
2-2 泛型類型的泛型參數(shù)必須在使用的時(shí)候給定出具體類型
泛型數(shù)據(jù)類型一定會(huì)含有類似剛才的 T
這樣的泛型參數(shù)。而這樣的參數(shù)是無(wú)法直接參與到運(yùn)行期間的代碼里的,因?yàn)樵谑褂茫▽?shí)例化和傳參)的時(shí)候,T
都將被改成一個(gè)具體的數(shù)據(jù)類型,這個(gè)是我們剛才的用法。
那么,有沒(méi)有一種可能,泛型參數(shù)不用給出具體類型,也可以參與運(yùn)行呢?實(shí)際上,C# 是不允許這么做的。這是因?yàn)榉盒蛥?shù)本身就是用來(lái)表達(dá)一種具體的數(shù)據(jù)類型。如果你不想實(shí)際給出它,那么你使用泛型的目的在哪里呢?泛型就是為了代替一種數(shù)據(jù)類型才存在的啊,那你不實(shí)例化,又想去使用它,這不是挺奇怪的嗎?所以,請(qǐng)注意這一點(diǎn)——C# 規(guī)定,所有的泛型參數(shù)在實(shí)例化的時(shí)候都必須給定具體的數(shù)據(jù)類型,比如,這么寫就是錯(cuò)誤的:
編譯器會(huì)直接跟你說(shuō),“我找不到 T
類型在哪里”。當(dāng)你以后看到編譯器告知你“找不到 T
類型”這樣的句子的時(shí)候,你就必須引起警覺(jué):我是不是沒(méi)有給出泛型參數(shù)的具體類型。而對(duì)于編譯器來(lái)說(shuō),由于所有的泛型參數(shù)都必須給定之后才可使用,所以它會(huì)假設(shè)你這里的 T
是一個(gè)實(shí)際的數(shù)據(jù)類型,然后參與運(yùn)算。但很遺憾的是,大多數(shù)時(shí)候我們都不會(huì)自己給一個(gè)數(shù)據(jù)類型取名為 T
,所以編譯器會(huì)告知你,“T
類型不存在”。
另外,我們這里要說(shuō)一個(gè)概念:開放泛型類型(Open Generic Type)和閉合泛型類型(Closed Generic Type)。所謂的開放泛型類型,就是泛型數(shù)據(jù)類型自身還沒(méi)有給定泛型參數(shù)的具體類型的時(shí)候,我們稱這樣的泛型叫做開放泛型類型。開放泛型類型有兩種表述的語(yǔ)法:
GenericType<T>
GenericType<>
其中第一種,是帶了一個(gè)泛型參數(shù)名叫做 T
的泛型類型。這個(gè) T
并非是一個(gè)具體我們已經(jīng)實(shí)現(xiàn)好了的一個(gè)數(shù)據(jù)類型,而是一個(gè)泛指的概念。就和之前解釋的那些 T
是一個(gè)意思。第二種,則是壓根不把 T
寫出來(lái)的另外一種表達(dá)它是泛型數(shù)據(jù)類型的方式。如果這個(gè)泛型包含一個(gè)泛型參數(shù)的話,由于之前我說(shuō)過(guò)一點(diǎn),泛型參數(shù)名稱是可以隨便取名的(只是我們經(jīng)常寫成 T
而已,但不是說(shuō)隨時(shí)隨地都必須寫 T
),因此既然是隨便取的名,那有時(shí)候也不一定取名為 T
,也可以是別的寫法,畢竟它只是一個(gè)代號(hào)。所以也經(jīng)常干脆就不寫泛型參數(shù)名稱,直接就可以表示出它是泛型參數(shù)。但此時(shí),表示泛型部分的尖括號(hào)仍然不可省略,否則外人看到這個(gè)類型不帶泛型部分的尖括號(hào),還以為它是一個(gè)非泛型的類型。
那么介紹完這個(gè)概念后,我們就可以把剛才的定義術(shù)語(yǔ)化了:C# 規(guī)定,所有的泛型數(shù)據(jù)類型在使用的具體使用時(shí)都必須是閉合的。
Part 3 如何決定泛型參數(shù)的意義——自己實(shí)現(xiàn)一個(gè)泛型數(shù)據(jù)類型
那么,剛才 List<T>
里我們知道了,它的用法是存儲(chǔ)一系列 T
類型的實(shí)例,而 T
具體是什么數(shù)據(jù)類型,由用戶在實(shí)例化的時(shí)候給出,這樣就可以確定了。不過(guò),為什么這個(gè) T
指的是 List<T>
集合里每一個(gè)元素的數(shù)據(jù)類型呢?下面我們就來(lái)說(shuō)一下,泛型參數(shù)的意義取決于什么。
泛型參數(shù)的意義和使用取決于你自己實(shí)現(xiàn)的代碼。如果我們定義好了一個(gè)泛型類型,而那么這個(gè)類型在任何一處都可以使用上這個(gè)泛型參數(shù),那么在哪里使用這個(gè)泛型參數(shù),那么這個(gè)泛型參數(shù)就有什么意義。換句話說(shuō),我現(xiàn)在只要自己實(shí)現(xiàn)了一個(gè)數(shù)據(jù)類型,那么這個(gè)類型是怎么用到 T
的,那么 T
就是什么意思。下面我們來(lái)實(shí)現(xiàn)一個(gè)具體的例子。
考慮一種情況。我們?cè)趫?zhí)行數(shù)據(jù)庫(kù)操作的時(shí)候,我們必然會(huì)讀取數(shù)據(jù)庫(kù)里的字段信息。倘若它是一個(gè)整型,那么就存在兩種可能:
它是一個(gè)實(shí)際的數(shù)據(jù),那么整型自然就可以表示出來(lái);
它在數(shù)據(jù)庫(kù)里沒(méi)有對(duì)應(yīng)的數(shù)值。也就是說(shuō)表格的這個(gè)位置是空格,沒(méi)有填入數(shù)據(jù)進(jìn)去。
如果你沒(méi)有接觸過(guò)數(shù)據(jù)庫(kù)操作,也不必關(guān)心這些細(xì)節(jié)。數(shù)據(jù)庫(kù)其實(shí)就是存儲(chǔ)了一張一張的表格,表格里有很多信息?,F(xiàn)在我想處理和調(diào)取里面的信息,這就是這個(gè)例子我想說(shuō)明的東西。
在處理的過(guò)程之中我們必然可能會(huì)遇到第二種情況,即表格的這個(gè)格子為空的情況。如果是空的,我們又想要必然返回出一個(gè)結(jié)果出來(lái)表示這個(gè)“無(wú)效數(shù)據(jù)”(或者“數(shù)值不明”)的情況,顯然,一般情況下,拿一個(gè)特殊值來(lái)表示就可以了,比如我們最喜歡的 0。
比如這樣的代碼,我隨便寫了幾句話,不管它能不能執(zhí)行,只用表達(dá)出我想表達(dá)的意思就可以了。整個(gè)行為就是在獲取表格的信息,然后按照表格的指定列名來(lái)獲取具體的信息。最后,返回這個(gè)信息數(shù)值就可以了。
不過(guò)問(wèn)題在于,假設(shè)我這個(gè) columnName
表示的是學(xué)生成績(jī)的話,那么獲取數(shù)據(jù)庫(kù)信息的時(shí)候,就必然會(huì)出現(xiàn)兩種情況:
學(xué)生的成績(jī)被正常記錄進(jìn)表格里;
學(xué)生由于種種原因未參加這次考試,因此表格里不包含這個(gè)學(xué)生的成績(jī)信息,因此這個(gè)格子為空。
那么,我如果通過(guò)上述的行為去獲取結(jié)果的話,那么有可能因?yàn)檫@個(gè)格子沒(méi)有信息而返回 0 作為默認(rèn)數(shù)值??蓡?wèn)題就在于這里。學(xué)生沒(méi)有考試不等于學(xué)生就是 0 分,因?yàn)榉謹(jǐn)?shù)數(shù)值記錄的是學(xué)生考試結(jié)果,而沒(méi)有考試并未包含在這個(gè)概念里面;另外,直接返回 0 分的話,我也不能從結(jié)果 0 分反推出原本學(xué)生到底是“考試考了但得了 0 分”還是“根本沒(méi)有參加考試”。
因此,我們需要再次想別的辦法去處理這個(gè)結(jié)果。我們最容易想到的是返回負(fù)數(shù)成績(jī)來(lái)表示學(xué)生情況,比如 -1 表示學(xué)生沒(méi)有參與考試,因?yàn)?-1 是學(xué)生考試不可能考出來(lái)的成績(jī)數(shù)值,因?yàn)樗环峡荚嚨梅值挠?jì)算規(guī)范規(guī)則。不過(guò),我這里為了介紹新語(yǔ)法,我們使用另外一個(gè)辦法來(lái)解決這樣的情況:可空值類型。
我們?cè)囍粋€(gè)“根本不可能為 null
值”的類型上添加一個(gè)新數(shù)值 null
作為取值范圍的其中一員。比如 int
的取值范圍是大約 -21 億到 +21 億的范圍,但期間包含負(fù)數(shù)、0和正數(shù)?,F(xiàn)在我追加一個(gè)叫做 null
的數(shù)值作為 int
的額外補(bǔ)充的數(shù)值進(jìn)去,這樣的話我們就有新的數(shù)值進(jìn)去了。
那么,我們假設(shè)這么去實(shí)現(xiàn)一個(gè)類型:
我們來(lái)仔細(xì)看看分析一下代碼是怎么個(gè)東西。首先我們寫了兩個(gè)構(gòu)造器、三個(gè)字段(兩個(gè)實(shí)例字段而且都是 private
的和一個(gè)靜態(tài)只讀字段)和三個(gè)屬性(都是 public
的)。
3-1 類型的語(yǔ)法
可以看到,我們?cè)跁鴮?Nullable<T>
類型的時(shí)候,我們是把 <T>
泛型部分寫在了 Nullable
非泛型部分的后面,整體是寫在類型的聲明上的,而仔細(xì)觀察代碼,你就會(huì)發(fā)現(xiàn),整個(gè)類型里,都沒(méi)有 <T>
的語(yǔ)法了,而只有使用 T
的一些地方,比如當(dāng)成參數(shù)傳入進(jìn)去、比如 default(T)
獲取 T
類型的默認(rèn)數(shù)值等。這是聲明一個(gè)泛型數(shù)據(jù)類型的基本書寫語(yǔ)法。
3-2 字段
先來(lái)分析字段。字段很簡(jiǎn)單,兩個(gè)字段:_isNull
和 _realValue
。其中:
_isNull
:表示我現(xiàn)在這個(gè)數(shù)據(jù)類型是不是表示的是表格里格子是空的情況。如果為空,我這里_isNull
就為true
;否則格子里有數(shù)值,則_isNull
一定為false
;_realValue
:表示我這個(gè)表格里存儲(chǔ)的真正的數(shù)值是多少。如果是空格,則_isNull
為true
的同時(shí),這個(gè)字段目前來(lái)說(shuō)就沒(méi)有任何意義。也就是說(shuō),_realValue
是當(dāng)且僅當(dāng)_isNull
為false
的時(shí)候才有用。
可以發(fā)現(xiàn)這兩個(gè)字段是相輔相成的,它們相互都有影響,不是單獨(dú)的存在。這個(gè)靜態(tài)只讀字段 Null
我們一會(huì)兒再來(lái)說(shuō)明,先來(lái)看構(gòu)造器。
3-3 構(gòu)造器
本類型包含兩個(gè)構(gòu)造器??梢钥吹秸Z(yǔ)法,因?yàn)樵陬愋吐暶魃弦呀?jīng)包含了泛型參數(shù)的聲明語(yǔ)句(Nullable<T>
的 <T>
泛型部分),因此我們可以認(rèn)為,整個(gè)數(shù)據(jù)類型都包含 T
的定義,因此在類型里,我們隨時(shí)隨地都可以使用 T
了,因此構(gòu)造器里我們只寫了 Nullable<T>
的非泛型部分,作為構(gòu)造器的名稱。這需要你稍微注意一下。C# 的語(yǔ)法是這么約定的:泛型數(shù)據(jù)類型的構(gòu)造器,類型名只書寫出泛型數(shù)據(jù)類型的非泛型部分。
再來(lái)看看構(gòu)造器的實(shí)現(xiàn)細(xì)節(jié)。第一個(gè)構(gòu)造器是只傳入一個(gè) T
類型的參數(shù)。在這個(gè)類型里面,我們?cè)O(shè)定了泛型參數(shù) T
的存在,那么這就表示我這個(gè) T
是一個(gè)假象的數(shù)據(jù)類型,它在以后會(huì)被一個(gè)具體的數(shù)據(jù)類型所替代。但是在實(shí)現(xiàn)代碼的時(shí)候,我們必須優(yōu)先給出具體的執(zhí)行步驟,畢竟先得有基本的代碼執(zhí)行邏輯,才能有后面的調(diào)用行為,對(duì)吧。于是,這里的 T
我們就可以拿來(lái)用了,而且是直接當(dāng)成具體類型來(lái)用。那么,第一個(gè)構(gòu)造器的意圖很明顯:就是為了實(shí)例化的時(shí)候能夠把一個(gè)基本的數(shù)值給拷貝進(jìn)去。而第二個(gè)呢?第二個(gè)是 private
修飾的,它只用來(lái)初始化數(shù)據(jù),給 _isNull
和 _realValue
提供賦值功能,也是為了給第一個(gè)構(gòu)造器進(jìn)行串聯(lián)調(diào)用。
3-4 屬性
接著來(lái)說(shuō)一下三個(gè)屬性。第一個(gè)屬性 IsNull
就是簡(jiǎn)單地封裝了一下 _isNull
字段,暴露 _isNull
字段數(shù)值。
然后是 RealValue
屬性。注意這個(gè)屬性的代碼就開始復(fù)雜起來(lái)了。這個(gè)屬性在調(diào)取 _realValue
字段之前,先判斷了一下 _isNull
的數(shù)值。剛才我們就說(shuō)過(guò),_isNull
決定了 _realValue
到底應(yīng)不應(yīng)該使用。如果 _isNull
為 true
,就意味著整個(gè)數(shù)據(jù)自己就是表示“空白”數(shù)據(jù),因此 _realValue
讀取出來(lái)就沒(méi)有意義了。但是 RealValue
屬性的意義就在于讀取 _realValue
的正確數(shù)值??扇绻?_isNull
為 true
的話,這種情況下 RealValue
屬性就沒(méi)有意義了,所以我們使用拋異常的方式告知使用方,這么使用是不正確的行為。這里的 InvalidOperationException
異常類型專門用于這種情況,所以非常合適作為拋異常的異常類型。
最后是 ValueOrDefault
屬性。這個(gè)屬性獲取的是 _realValue
的結(jié)果,但沒(méi)有拋異常。如果 _isNull
為 true
,那么這個(gè)屬性返回的結(jié)果是 default(T)
而并不是拋異常。這是 ValueOrDefault
屬性和 RealValue
屬性執(zhí)行行為的差異點(diǎn)。
3-5 如何使用 Nullable<T>
類型?
使用 Nullable<T>
類型,那么就這么做:
大概這么就可以了。這里要說(shuō)的地方有兩個(gè)語(yǔ)法點(diǎn):(Nullable<int>)
強(qiáng)轉(zhuǎn)符號(hào)和 Nullable<int>.Null
這個(gè)泛型類型里的靜態(tài)只讀字段。
其實(shí)也沒(méi)啥好說(shuō)的,你把泛型類型當(dāng)成普通類型使用就行了,不要去歧視它,但是總有小伙伴入門學(xué)習(xí)的時(shí)候沒(méi)轉(zhuǎn)過(guò)這個(gè)彎來(lái)。首先說(shuō)一下強(qiáng)轉(zhuǎn)。假定我們這個(gè) GetResult
方法返回一個(gè)結(jié)果,但不是我們想要的結(jié)果,比如它返回的是 int
類型的結(jié)果來(lái)表示表格的數(shù)值;然后使用拋 CellIsEmptyException
異常來(lái)表示表格的對(duì)應(yīng)位置是空格。那么我們知道了行為之后,就使用 try
-catch
的機(jī)制來(lái)捕獲異常,并改寫轉(zhuǎn)換為返回 Nullable<int>
類型結(jié)果。可是我們都知道,int
和 Nullable<int>
在 C# 語(yǔ)法里是不受支持的:直接使用強(qiáng)制轉(zhuǎn)換是無(wú)效的,因?yàn)?C# 只允許具有繼承派生關(guān)系的數(shù)據(jù)類型,以及系統(tǒng)預(yù)定義的那些基本數(shù)據(jù)類型才擁有轉(zhuǎn)換機(jī)制。而 int
是預(yù)定義的類型,但 Nullable<int>
卻不是。因此,我們可以通過(guò)在 Nullable<T>
類型里定義強(qiáng)制類型轉(zhuǎn)換符的方式來(lái)達(dá)到 int
轉(zhuǎn)換為 Nullable<int>
的方式。我們只需要在代碼里補(bǔ)充這一段代碼:
這樣的話,T
就可以直接轉(zhuǎn)換為 Nullable<T>
了。因?yàn)槲疫@里用的是 explicit
關(guān)鍵字,因此我們必須使用強(qiáng)制轉(zhuǎn)換的運(yùn)算符 (Nullable<int>)
來(lái)進(jìn)行轉(zhuǎn)換。另外請(qǐng)注意一點(diǎn)。在這段代碼里,我們使用的是泛型參數(shù)名 T
臨時(shí)占位,但在具體推導(dǎo)和使用的時(shí)候,我們是實(shí)際的 int
類型。因此 C# 會(huì)自動(dòng)識(shí)別,并將 T
改成 int
,所以上面強(qiáng)制轉(zhuǎn)換運(yùn)算符就寫成 (Nullable<int>)
而不是 (Nullable<T>)
甚至是 (Nullable<>)
了,這一點(diǎn)要注意。
另外,如果我用的是 implicit
關(guān)鍵字的話,T
就允許直接轉(zhuǎn) Nullable<T>
了,所以如果使用這個(gè)關(guān)鍵字的話,即使我們直接不寫強(qiáng)制轉(zhuǎn)換符號(hào),C# 也會(huì)自動(dòng)轉(zhuǎn)換過(guò)去,所以這樣做更方便一些:
比如這樣。
這是第一個(gè)語(yǔ)法。接下來(lái)說(shuō)一下第二個(gè)語(yǔ)法。我們寫的是 Nullable<int>.Null
,有點(diǎn)別扭,不過(guò)是正常的寫法。首先我們要獲取里面的 Null
字段的數(shù)值作為默認(rèn)結(jié)果返回出去,以表達(dá)這個(gè)單元格為空??蓡?wèn)題在于,我直接寫 Nullable<T>.Null
甚至于是 Nullable<>.Null
是不是少了點(diǎn)東西?是的,泛型參數(shù)的實(shí)際類型尚未指定。雖然這里不屬于實(shí)例化過(guò)程,也就是之前的 new
語(yǔ)句,但不指定泛型參數(shù)的話,我咋知道這個(gè) Null
字段的具體類型是什么呢?它里面包裝的 _realValue
又是什么類型的呢?所以,類似這樣的情景,哪怕它不是實(shí)例化語(yǔ)句,但也需要你必須指定泛型參數(shù)的實(shí)際類型才可以繼續(xù)書寫后續(xù)代碼,而此時(shí),這個(gè)泛型部分 <int>
應(yīng)該寫在泛型類型的后面,而不是 Null
字段名的后面,這是顯然的,畢竟是 Nullable<>
是泛型類型,而 Null
字段它只是一個(gè)字段,它不是泛型的,我們只能說(shuō)它的類型是泛型的類型,而字段本身并非泛型的。
3-6 實(shí)際上 C# 已經(jīng)自帶了這個(gè) API 了
是的,我們?cè)跁鴮懘a的時(shí)候,教大家自己實(shí)現(xiàn)了一下 Nullable<>
類型以告訴你,泛型的語(yǔ)法都是怎么去使用的。但是因?yàn)檫@個(gè)數(shù)據(jù)類型也經(jīng)常被使用到,因此 Nullable<>
類型實(shí)際上在 C# 的 API 里面是自帶的。也就是說(shuō),它自帶一個(gè) API 允許我們直接使用,而無(wú)需我們每次都去自己實(shí)現(xiàn)一下。而且好巧不巧,它就叫 Nullable<T>
,跟我這里取名是一樣的。因此,以后寫代碼的時(shí)候,你遇到這種必須要用這個(gè)數(shù)據(jù)類型的場(chǎng)合下,你就可以直接拿著用了。
不過(guò),C# 不止單純地提供了這個(gè)數(shù)據(jù)類型這么簡(jiǎn)單,它還提供了一些新的語(yǔ)法,就是配套這個(gè)數(shù)據(jù)類型使用的語(yǔ)法,用來(lái)簡(jiǎn)化代碼,增強(qiáng)可讀性,這個(gè)將在下一節(jié)內(nèi)容給大家介紹到。
Part 4 泛型參數(shù)的細(xì)節(jié)
下面我們來(lái)討論一些有意義的、對(duì)以后我們繼續(xù)使用泛型數(shù)據(jù)類型有幫助的,作為銜接的、泛型參數(shù)有關(guān)的處理細(xì)節(jié),并作為問(wèn)題答疑呈現(xiàn)出來(lái)。
4-1 可否使用 void
作為泛型參數(shù)的實(shí)際類型?
這不廢話嘛。void
在 C# 里表示的是無(wú)返回值,那么從這個(gè)角度來(lái)說(shuō)的話,void
壓根不屬于一個(gè)數(shù)據(jù)類型,因此你無(wú)法把 void
當(dāng)成數(shù)據(jù)類型作為泛型參數(shù)的實(shí)際類型。這也就是說(shuō),這么寫是錯(cuò)誤的:
不僅是錯(cuò)的,而且錯(cuò)得離譜。
4-2 實(shí)際上,指針類型也不能
另外,實(shí)際上不只是這樣。指針類型也不允許作為泛型參數(shù)的實(shí)際類型,也就是說(shuō),類似 List<int*>
這樣的語(yǔ)法在 C# 里也是不可行的寫法。這是因?yàn)榉盒蛥?shù)自身要體現(xiàn)的是一個(gè)可以靈活使用的數(shù)據(jù)類型,而指針并不滿足:它只用于表達(dá)數(shù)據(jù)指向,以及變動(dòng)指針的指向來(lái)達(dá)到遍歷數(shù)據(jù)的過(guò)程。除了這樣的用法外,別無(wú)其它。它不夠靈活,甚至指針類型連裝箱和拆箱機(jī)制都不存在。也就是說(shuō),你無(wú)法把指針類型賦值給 object
類型,這我之前就說(shuō)過(guò)了:指針甚至不屬于 object
類型的派生體系。所以,在數(shù)據(jù)類型體系里,指針類型是唯一一種無(wú)法作為泛型參數(shù)的實(shí)際類型的數(shù)據(jù)類型。當(dāng)然,話也不能說(shuō)絕對(duì)了,因?yàn)橐院髸?huì)誕生一些新的語(yǔ)法,這個(gè)規(guī)則會(huì)被打破。不不不,我的意思是會(huì)有更多無(wú)法作為泛型參數(shù)的實(shí)際類型的數(shù)據(jù)類型,而不是允許讓指針類型成為泛型參數(shù)的實(shí)際類型。
4-3 泛型參數(shù)的對(duì)應(yīng)具體類型可否仍為一個(gè)泛型類型?
答案是可以的。任何數(shù)據(jù)類型都從 object
派生,而泛型數(shù)據(jù)類型在 C# 語(yǔ)言設(shè)計(jì)里也是如此。這個(gè) T
在正常情況下都不會(huì)約束你到底放什么數(shù)據(jù)類型進(jìn)去,所以,泛型數(shù)據(jù)類型也是可以作為泛型參數(shù)的具體類型來(lái)使用的。比如說(shuō),我有一個(gè)列表(假設(shè)是 List<>
類型來(lái)實(shí)現(xiàn)),由于列表的元素是可以為任何的數(shù)據(jù)類型的,因此有些時(shí)候我們會(huì)遇到一種情況,就是列表的每一個(gè)元素也都是一個(gè)列表,就好像是一個(gè)鋸齒數(shù)組一樣,每一個(gè)數(shù)組元素都是一個(gè)數(shù)組。這個(gè)時(shí)候,假設(shè)每一個(gè)元素都是一個(gè) List<int>
的列表類型的話,那么我們的語(yǔ)法是這樣的:
這個(gè)語(yǔ)法看起來(lái)就有點(diǎn)抽象看不懂了。請(qǐng)仔細(xì)觀察。我們把最基本的 List

實(shí)際上,泛型參數(shù) T
的實(shí)際類型是被這個(gè)尖括號(hào)里的 List<int>
給代替掉了。所有類似這樣的嵌套泛型數(shù)據(jù)類型的類型都是這樣去理解和分析。不過(guò),我們用到的時(shí)候還會(huì)再說(shuō),因此這里了解一下這個(gè)記法就可以了。
對(duì),這樣嵌套泛型類型的泛型數(shù)據(jù)類型稱為嵌套泛型數(shù)據(jù)類型(Nested Generic Type)。
那么這里就順帶一提嵌套泛型數(shù)據(jù)類型的閉合性和開放性。假設(shè)我這么寫代碼:
這樣的代碼對(duì)不對(duì)呢?答案是不對(duì)的。因?yàn)檎w來(lái)說(shuō),整體 List<>
類型指定了一個(gè)泛型參數(shù)的實(shí)際類型。不過(guò),這個(gè)實(shí)際類型并未真正的“實(shí)際”:它是開放類型,因?yàn)?T
并沒(méi)有指定。因此,如果遇到這樣的情況,你一定要注意,嵌套泛型數(shù)據(jù)類型必須所有層次的實(shí)際類型都必須是閉合的。
4-4 泛型參數(shù)是否可以有多個(gè)?
如題。實(shí)際上是允許的。C# 允許我們有任意多的泛型參數(shù),只要你給定的泛型參數(shù)名字取得不重復(fù)就行。實(shí)際上,在 API 里確實(shí)存在一個(gè)使用多泛型參數(shù)的數(shù)據(jù)類型,它稱為字典(Dictionary)。所謂的字典,就是你數(shù)據(jù)結(jié)構(gòu)里面學(xué)到的哈希表(Hash Table),不過(guò) C# 的 API 更想取一個(gè)更容易初學(xué)理解的名字,所以叫它字典。
字典是包含兩個(gè)泛型參數(shù)的泛型數(shù)據(jù)類型,你想想看,我們實(shí)際生活中使用的字典就是先查到對(duì)應(yīng)的詞語(yǔ)(或者漢字),然后后面會(huì)給出它的釋義,這么一個(gè)過(guò)程。而字典這個(gè)數(shù)據(jù)類型,它具備完全一致的行為和操作。它實(shí)例化出來(lái)之后,我們通過(guò) Add
方法往里加入數(shù)據(jù)進(jìn)去。在需要查找信息的時(shí)候,我們直接通過(guò)一個(gè)叫做鍵(Key)的東西來(lái)查閱里面的信息。這個(gè)鍵對(duì)應(yīng)了我們實(shí)際生活之中查字典的這個(gè)單詞(或者漢字)。因?yàn)閱卧~(或漢字)本身是不同的,所以每一個(gè)單詞(或漢字)都會(huì)給予一個(gè)義項(xiàng)。在查閱的時(shí)候只需要找到單詞(或漢字),就可以查閱解釋了。這個(gè)字典也是如此。每一個(gè)鍵都對(duì)應(yīng)了一個(gè)叫做值(Value)的東西,也就是結(jié)果的意思。只要我們找到了鍵,就可以看到對(duì)應(yīng)的值,這就是字典的實(shí)際使用。
不過(guò)這些 API 我們以后會(huì)陸續(xù)介紹到,光說(shuō)的話是有點(diǎn)抽象的,因此我不打算在這里展開介紹字典的內(nèi)容,但我要告訴大家的是,多泛型參數(shù)的數(shù)據(jù)類型,這些泛型參數(shù)都是如何羅列和表示的。實(shí)際上,在代碼里,我們是這么書寫一個(gè)多泛型參數(shù)的數(shù)據(jù)類型的:
是的,直接和普通參數(shù)的寫法類似,尖括號(hào)里羅列所有的泛型參數(shù),然后用逗號(hào)分開就可以了。
在字典類型的聲明里,我們不外乎就是把這里具體的 string
和 int
給改成一個(gè)泛型參數(shù)名而已,所以寫法類似:
只不過(guò),這里的 TKey
和 TValue
都不再叫做 T
,一來(lái)是泛型參數(shù)名可以隨意取,二來(lái)是為了有一個(gè)更具有可讀性的名字會(huì)幫助我們書寫代碼的時(shí)候更有效率,因此字典的兩個(gè)泛型參數(shù)名一個(gè)叫做 TKey
一個(gè)叫做 TValue
,都不是 T
了。
然后。如果要說(shuō)字典類型的話,你說(shuō)字典,別人也可以想到 Dictionary<TKey, TValue>
類型。但是如果你要直接說(shuō)類型名的話,和之前說(shuō)的兩種表達(dá)一樣,你可以說(shuō) Dictionary<TKey, TValue>
類型,也可以說(shuō) Dictionary<,>
類型,即省去泛型參數(shù)名,對(duì)方看到這個(gè)說(shuō)法也都能知道你在說(shuō)什么。
這也只是告訴大家一個(gè)多泛型參數(shù)的泛型數(shù)據(jù)類型應(yīng)該如何聲明。這個(gè)就是一個(gè)例子。
最后,思考一個(gè)問(wèn)題。請(qǐng)問(wèn) Dictionary<int, Dictionary<string, double[]>>
這個(gè)嵌套泛型數(shù)據(jù)類型作為實(shí)例化對(duì)象后,什么部分都對(duì)應(yīng)什么類型,你能看明白嗎?假設(shè)這個(gè)數(shù)據(jù)類型用于存儲(chǔ)一個(gè)人的數(shù)據(jù)信息(int
是學(xué)號(hào),string
是人名,double[]
是一組成績(jī)),那么整個(gè)數(shù)據(jù)類型表達(dá)了一個(gè)什么樣的信息呢?
4-5 泛型參數(shù)不變性
還記得數(shù)組協(xié)變的規(guī)則嗎?數(shù)組在任何時(shí)候都可以按數(shù)組為單位將元素轉(zhuǎn)換為更模糊的類型。例如 string[]
可以隱式轉(zhuǎn)換為 object[]
類型。但問(wèn)題是,隱式轉(zhuǎn)換之后,object[]
類型拿到手就無(wú)法確定里面的每個(gè)元素的具體類型了,因此如果你想修改里面的元素,一旦元素的類型對(duì)不上,C# 就會(huì)引發(fā)異常 ArrayTypeMismatchException
。
這個(gè)設(shè)計(jì)初衷是好的,但未免有些奇怪。因?yàn)殡[式轉(zhuǎn)換過(guò)去是正確的現(xiàn)象,可我無(wú)法確定里面的具體類型,所以就會(huì)出現(xiàn)奇怪的異常。因此,C# 的泛型都是不變的。
所謂的不變性(Invariance),指的是泛型的數(shù)據(jù)類型的實(shí)例在初始化的時(shí)候,無(wú)法改變泛型參數(shù)的實(shí)際類型,比如這樣:
左邊是 List<object>
類型,但右邊是 List<string>
。泛型參數(shù)發(fā)生了變化,而 C# 不允許我們這么賦值。這就是所謂的泛型參數(shù)的不變性??紤]一下,如果這樣的賦值是允許的話,我們?cè)诤罄m(xù)使用 list
變量的時(shí)候,都會(huì)按照 List<object>
來(lái)參考和使用??煞盒蛥?shù)現(xiàn)在是 object
,我隨意往里面加一個(gè)元素進(jìn)去:
30 是 int
類型的字面量。我們右側(cè)賦值的實(shí)例是 List<string>
類型的,意味著我們每一個(gè)元素都得是 string
類型。而左邊接收方卻又允許我們按 List<object>
來(lái)繼續(xù)使用,那么我添加一個(gè) 30 的話,object
來(lái)說(shuō)是兼容的:因?yàn)?int
可以往 object
上隱式轉(zhuǎn)換,所以沒(méi)有問(wèn)題;可問(wèn)題在于,我原始的數(shù)據(jù)類型可是 string
啊,賦值一個(gè) int
就會(huì)引起整個(gè)泛型的體系直接瓦解,而官方美其名曰“泛型的使用不安全”。因此,C# 不允許泛型參數(shù)的協(xié)變。
反之,協(xié)變的對(duì)應(yīng)概念是逆變。逆變就是倒過(guò)來(lái)的邏輯:從當(dāng)前泛型參數(shù)變?yōu)楦_的類型。
右側(cè)的泛型參數(shù)是 object
類型,賦值卻變?yōu)榱?string
。你認(rèn)為這個(gè)賦值過(guò)程是正確的嗎?好像也不是特別合理。雖然接收方改成了 string
了之后賦值好像沒(méi)啥問(wèn)題(一個(gè)原本存儲(chǔ) object
元素的集合大不了暴露給用戶看的時(shí)候讓用戶只存 string
就行了,反正 string
是 object
的一個(gè)派生類型),但是未免有些奇怪了。
總之,C# 也沒(méi)有采用這種賦值方式。因此,在 C# 的世界里,泛型是不變的。泛型不變暗指的就是,泛型參數(shù)自身的類型的實(shí)例化類型部分和接收方的類型部分得是一致的。
4-6 泛型類型的可重載性
雖然這么取名不合適,但是它跟重載是一個(gè)差不多的概念。假設(shè)我有一個(gè)類型 A
,還有一個(gè) A<T>
類型,帶有一個(gè)泛型參數(shù)。那么,同名的 A
類型能否放在一起成為兩個(gè)不同的數(shù)據(jù)類型嗎?答案是可以的。這個(gè)是泛型參數(shù)的可重載性。泛型參數(shù)可以任意多,也可以沒(méi)有。但是沒(méi)有的時(shí)候不能寫為空的尖括號(hào),而是去掉這個(gè)泛型參數(shù)的尖括號(hào)。所以,A
和 A<T>
是兩個(gè)完全不同的數(shù)據(jù)類型,雖然它們重名,在 C# 里也是允許并存的。
之后我們會(huì)廣泛出現(xiàn)這樣的現(xiàn)象,比如 IEnumerable<T>
接口和 IEnumerable
接口。一個(gè)泛型的一個(gè)非泛型的。
不過(guò),這里要稍微說(shuō)一下的是,這種重載的概念和以往學(xué)習(xí)的重載有所不同。泛型參數(shù)哪怕你取不一樣的名字,因?yàn)樗皇怯脕?lái)代指一個(gè)具體的類型,因此名字一不一樣并不能區(qū)分為不同的類型。舉個(gè)例子,A<T>
、A<GenericTypeArg>
和 A<TypeArgument>
的泛型參數(shù)名不同,并非它們是不同的類型,因?yàn)樗鼈兊綍r(shí)候也都會(huì)被代替為具體的類型,因此它們是同一種類型,不構(gòu)成重載。所以,泛型類型的重載規(guī)則是看泛型參數(shù)的個(gè)數(shù)不一致來(lái)表現(xiàn)的,比如帶有 1 個(gè)泛型參數(shù)的泛型類型 A<>
和 A
(非泛型類型)是構(gòu)成重載的類型的。