最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

第 67 講:C# 2 之泛型(一):泛型的基本使用

2021-11-25 16:49 作者:SunnieShine  | 我要投稿

歡迎大家來(lái)到 C# 的新語(yǔ)法板塊。C# 的新語(yǔ)法非常多,多到三言兩語(yǔ)無(wú)法說(shuō)完。我大概數(shù)了一下,才到 C# 8,新語(yǔ)法就已經(jīng)超過(guò)一百個(gè)了。大家可以參考這個(gè)頁(yè)面獲取所有的新語(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ō),ArrayListList<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è)整型,那么就存在兩種可能:

  1. 它是一個(gè)實(shí)際的數(shù)據(jù),那么整型自然就可以表示出來(lái);

  2. 它在數(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)兩種情況:

  1. 學(xué)生的成績(jī)被正常記錄進(jìn)表格里;

  2. 學(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ù)值是多少。如果是空格,則 _isNulltrue 的同時(shí),這個(gè)字段目前來(lái)說(shuō)就沒(méi)有任何意義。也就是說(shuō),_realValue 是當(dāng)且僅當(dāng) _isNullfalse 的時(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)該使用。如果 _isNulltrue,就意味著整個(gè)數(shù)據(jù)自己就是表示“空白”數(shù)據(jù),因此 _realValue 讀取出來(lái)就沒(méi)有意義了。但是 RealValue 屬性的意義就在于讀取 _realValue 的正確數(shù)值??扇绻?_isNulltrue 的話,這種情況下 RealValue 屬性就沒(méi)有意義了,所以我們使用拋異常的方式告知使用方,這么使用是不正確的行為。這里的 InvalidOperationException 異常類型專門用于這種情況,所以非常合適作為拋異常的異常類型。

最后是 ValueOrDefault 屬性。這個(gè)屬性獲取的是 _realValue 的結(jié)果,但沒(méi)有拋異常。如果 _isNulltrue,那么這個(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é)果。可是我們都知道,intNullable<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ǔ)法是這樣的:

List<List<int>> 這個(gè)語(yǔ)法看起來(lái)就有點(diǎn)抽象看不懂了。請(qǐng)仔細(xì)觀察。我們把最基本的 List 和它的泛型部分使用的尖括號(hào)給拿走,就可以發(fā)現(xiàn):

實(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)分開就可以了。

在字典類型的聲明里,我們不外乎就是把這里具體的 stringint 給改成一個(gè)泛型參數(shù)名而已,所以寫法類似:

只不過(guò),這里的 TKeyTValue 都不再叫做 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 就行了,反正 stringobject 的一個(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)。所以,AA<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)成重載的類型的。


第 67 講:C# 2 之泛型(一):泛型的基本使用的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
惠来县| 卓尼县| 梧州市| 潢川县| 颍上县| 诸暨市| 新巴尔虎右旗| 青州市| 女性| 苍梧县| 闻喜县| 嘉荫县| 边坝县| 普兰店市| 栾城县| 威信县| 军事| 沂源县| 修水县| 青田县| 缙云县| 博爱县| 开原市| 潞城市| 虎林市| 康平县| 阿拉善盟| 镇坪县| 鄢陵县| 邓州市| 海林市| 吉水县| 陆丰市| 大同市| 青浦区| 酉阳| 繁昌县| 嘉兴市| 荥阳市| 黔江区| 柘荣县|