第 68 講:C# 2 之泛型(二):可空值類型
今天我們來銜接前一節(jié)內(nèi)容說到的 Nullable<T>
類型。這個(gè)數(shù)據(jù)類型在 C# 里是提供了這個(gè) API 的,名字也一樣,用法是類似的,因此我們需要單獨(dú)提出來給大家解釋和說明。
Part 1 簡(jiǎn)化類型聲明語法:可空值類型的概念
可空值類型(Nullable Value Type,簡(jiǎn)稱 NVT)是什么呢?它表示一個(gè)值類型,但它可以表示“為空”的情況。就像是上一節(jié)內(nèi)容里說到的表格里取出來的單元格為空一樣,最終獲取到的結(jié)果數(shù)值為“空”,我們使用的是 null
這個(gè)常用的字面量來表達(dá)這個(gè)數(shù)據(jù)類型為空的情況。
1-1 可空類型記號(hào) ?
C# 2 里除了泛型的概念外,還為這個(gè)數(shù)據(jù)類型單獨(dú)設(shè)計(jì)了一個(gè)語法。如果我們要聲明一個(gè)可空值類型,都不得不寫全名:Nullable<T>
,然后把 T
替換為我們的實(shí)際值類型,比如 int
。
可問題在于這么寫太麻煩了,因?yàn)槊看味家蜻@么多字母。因此 C# 提供了一個(gè)新的類型記號(hào):?
。之前我們學(xué)習(xí)過的類型記號(hào)一共有三個(gè):
T[]
:表示元素為T
類型的數(shù)組;T<>
:表示T
是泛型數(shù)據(jù)類型;T*
:表示指向一個(gè)T
類型的指針。
現(xiàn)在我們可以使用新的記號(hào) ?
來聲明一個(gè)數(shù)據(jù)類型:T?
,它表示一個(gè)值類型是可空的。請(qǐng)注意,這個(gè) T?
的 T
此時(shí)只能是一個(gè)值類型。引用類型由于它自身的特性,它自己默認(rèn)數(shù)值就是 null
,因此完全不需要 ?
記號(hào)自身就可以表達(dá)出 null
的數(shù)值,因此,這個(gè)記號(hào)只對(duì)值類型有效:
這樣的語法。這個(gè) int?
將直接被翻譯成 Nullable<int>
,也就是說,它倆是等價(jià)的寫法,只不過 int?
更簡(jiǎn)單一些。沒人會(huì)因?yàn)閮蓚€(gè)完全一樣的語法,卻選擇了更難書寫的 Nullable<int>
吧?
由于它完全等價(jià)于 Nullable<int>
,所以:
是原本 int? val = new int?(30);
的等價(jià)完整版寫法。
1-2 null
字面量在可空值類型里的應(yīng)用
接著。如果我要賦 Null
類似的數(shù)值,那么怎么做呢?C# 的行為是這樣的:直接用關(guān)鍵字 null
來臨時(shí)代替這種情況。也就是說,null
直接表示一個(gè) T?
里的特殊值。
比如這樣就行了。
1-3 Nullable<T>
類型自身不允許嵌套使用
另外說一點(diǎn)要注意的地方。按照定義,T?
的 T
可以是任何值類型。但 T?
自己會(huì)被翻譯成 Nullable<T>
類型,而 Nullable<T>
也是一個(gè)值類型。那么是不是就意味著它自己也可以作為 T
而替換過去,使之成為一個(gè)嵌套泛型數(shù)據(jù)類型呢?答案是否定的。因?yàn)?Nullable<T>
是特殊的數(shù)據(jù)類型,它自身代表有 null
數(shù)值的值類型。但它自己自身包含 null
數(shù)值,所以如果它自己再嵌套進(jìn)去作為 T?
的 T
出現(xiàn)的話,就顯得沒有任何意義了。因此,C# 不允許 Nullable<Nullable<T>>
的語法存在,當(dāng)然,T??
這樣的語法也就不存在了。
Part 2 Nullable<T>
類型的常用成員介紹
使用的時(shí)候,我們要介紹一下這個(gè)類型的真正的 API 有哪些。
2-1 T?(T)
構(gòu)造器
Nullable<T>
類型(或者以后也可以直接簡(jiǎn)寫成 T?
了)只有一個(gè)構(gòu)造器,傳入一個(gè) T
類型的實(shí)例作為參數(shù)。也就是像是剛才那樣,我使用 new int?(30)
來實(shí)例化一個(gè) 30 這個(gè) int
類型的數(shù)據(jù)作為 int?
類型的底層數(shù)值。
2-2 HasValue
和 Value
屬性
貼心的 T?
類型提供了 HasValue
和 Value
屬性用于獲取里面的數(shù)值。和之前我們自己設(shè)計(jì)的 _realValue
的邏輯類似,HasValue
屬性表示獲取這個(gè) T?
類型的實(shí)例到底是不是包含數(shù)值。我們之前是認(rèn)為,_isNull
為 true
就表示 _realValue
是“沒有值”的狀態(tài),而這里 T?
類型封裝好了一系列的東西,所以 HasValue
可以立刻獲取實(shí)例是不是包含數(shù)值。而 Value
數(shù)值的效果和我們?cè)O(shè)計(jì)的 RealValue
屬性效果完全一致:如果有數(shù)值,就返回結(jié)果;否則直接拋出 InvalidOperationException
類型的異常告知用戶,因?yàn)?T?
不包含值,而你嘗試在取它的值。
是這么使用的。
2-3 GetValueOrDefault
實(shí)例方法
API 還提供了 GetValueOrDefault
方法,這個(gè)方法具有兩個(gè)重載版本,一個(gè)是無參的,一個(gè)是帶一個(gè) T
類型的參數(shù)的。這個(gè)方法和我們之前設(shè)計(jì)的屬性 ValueOrDefault
是一致的效果,不過由于有兩個(gè)重載版本,所以它只和無參這個(gè)重載版本是一致的執(zhí)行效果,而帶 T
類型參數(shù)的這個(gè)重載版本則稍微不一樣。
如果我們認(rèn)為 GetValueOrDefault()
的執(zhí)行表達(dá)式是 _isNull ? default(T) : _realValue
的話,那么帶 T
參數(shù)的重載版本的執(zhí)行表達(dá)式是 _isNull ? parameter : _realValue
。
2-4 ToString
方法
這個(gè) T?
類型在底層也重寫了 object
派生下來的那些方法,不過 Equals
和 GetHashCode
就不多說了,因?yàn)槲覀兓旧弦灿貌簧纤鼈?;?ToString
稍微可以提一下。
ToString
方法會(huì)輸出一個(gè) T?
的實(shí)際數(shù)值。如果 T?
實(shí)例里包含的是 null
數(shù)值,那么 ToString
也不會(huì)因?yàn)榘?null
而拋異常,但輸出的結(jié)果是一個(gè)空字符串,因此你可能看不到有任何東西顯示出來。
2-5 類型轉(zhuǎn)換器
T?
類型還提供了兩個(gè)轉(zhuǎn)換器,一個(gè)是從 T
轉(zhuǎn) T?
的,另外一個(gè)則是從 T?
轉(zhuǎn) T
的。在 API 里,從 T
往 T?
轉(zhuǎn)換的是隱式轉(zhuǎn)換,而 T?
到 T
的轉(zhuǎn)換是顯式轉(zhuǎn)換。這也就是說,我們?nèi)绻o一個(gè) int?
類型賦值 int
數(shù)值的時(shí)候,是可以直接書寫的:
val
是不是真的包含值而非 null
是的,T?
往 T
上強(qiáng)制轉(zhuǎn)換等價(jià)于直接調(diào)用 Value
屬性。因此這里需要你注意,它不會(huì)調(diào)用 GetValueOrDefault
方法,而是調(diào)用 Value
屬性。如果 int?
實(shí)例不包含任何數(shù)值(即 null
)的話,強(qiáng)制轉(zhuǎn)換將會(huì)產(chǎn)生 InvalidCastException
異常,表示你的強(qiáng)制轉(zhuǎn)換是失敗的。
Part 3 對(duì)自帶運(yùn)算符的數(shù)據(jù)類型,T?
的處理過程
我們常見的 T
的替換數(shù)據(jù)類型一般就是比如 int
啊、float
啊、bool
這些數(shù)據(jù)類型。雖然 T
可以被任何值類型所替換,但實(shí)際上基本上用不上自定義值類型作為 T
替代的情景。不是說語法不允許,只是很少用。
而正是因?yàn)?T
經(jīng)常被內(nèi)置值類型所替代,所以 T
類型的運(yùn)算符處理過程,T?
也具備。換句話說,比如我 int
類型有加法運(yùn)算,那么 int?
的實(shí)例其實(shí)也具備加法運(yùn)算操作,你甚至可以混合加法運(yùn)算,一個(gè) int
一個(gè) int?
都行。不過,這種運(yùn)算過程是如何的呢?
C# 是這么設(shè)計(jì)計(jì)算規(guī)則的。在操作過程之中,但凡有一個(gè)實(shí)例是 null
的話,操作就會(huì)立刻得到 null
作為結(jié)果,否則,將操作的實(shí)例的真正數(shù)值取出作為處理,并得到結(jié)果。
請(qǐng)問,這個(gè)例子輸出結(jié)果是多少?是的,40。
這個(gè)例子呢?請(qǐng)注意 (a + b).ToString()
這個(gè)表達(dá)式。因?yàn)?(a + b)
是一個(gè)部分,而后面的 .ToString()
是一部分,按照運(yùn)算符優(yōu)先級(jí),我們應(yīng)當(dāng)先計(jì)算 (a + b)
這部分。而按照 C# 語法設(shè)計(jì),a
和 b
里有一個(gè)是 null
,因此結(jié)果為 null
。
問題來了。null
是結(jié)果,但它作為表達(dá)式結(jié)果的實(shí)例去調(diào)用 ToString
方法,不會(huì)拋異常嗎?真的不會(huì)出現(xiàn) NullReferenceException
異常嗎?是的??煽罩殿愋筒粫?huì)出現(xiàn)這個(gè)異常。但是請(qǐng)注意,它作為結(jié)果來看的話,因?yàn)榻Y(jié)果是 null
,所以按照可空值類型的調(diào)用 ToString
方法的規(guī)則來看,最終輸出的結(jié)果是一個(gè)空字符串??梢?yàn)榭兆址鞘裁炊紱]有的字符串,所以輸出內(nèi)容里,你也看不到任何可見字符。
所以,所有運(yùn)算符的處理規(guī)則和運(yùn)算規(guī)則均和這里的操作是一致的,除了……
Part 4 bool?
類型和三值布爾的概念
有一個(gè)可空值類型,它可能有些特殊,因?yàn)樗奶幚硪?guī)則不完全符合上面的所說的那些東西,這個(gè)數(shù)據(jù)類型叫 bool?
。
bool?
類型是 bool
類型的可空版本,也就是說,bool?
包含三個(gè)可能取值:true
、false
和 null
,除此之外,別無其它。正是因?yàn)樗娜≈捣秶挥腥齻€(gè)情況,所以它的處理機(jī)制有些特殊,也被編譯器自身處理和優(yōu)化掉了。另外,由于它有三個(gè)情況可取,所以 bool?
有一個(gè)單獨(dú)的名稱叫三值布爾類型(Tri-valued Boolean)。
三值布爾擁有三種情況,而布爾運(yùn)算有 &
和 |
兩種最為常見,在 C# 里,三值布爾運(yùn)算就顯得特別特殊了。在三值布爾運(yùn)算里,不是一方為 null
結(jié)果就一定是 null
。我們來看表格:

該表記錄了 x
和 y
兩個(gè)三值布爾對(duì)象的 &
和 |
的結(jié)果??梢宰⒁獾剑?/span>true | null
是為 true
而不是為 null
的。
有人問為什么沒有異或運(yùn)算
^
。異或運(yùn)算的處理機(jī)制和 C# 原生的bool
是一致的,而如果其中一方為null
,那么異或運(yùn)算結(jié)果則為null
,它是滿足前述內(nèi)容的運(yùn)算,因此這里沒有單獨(dú)列出。
這個(gè)表格怎么記呢?很簡(jiǎn)單,不要死記硬背。
首先我們知道基本的不空運(yùn)算結(jié)果,這個(gè)不必多說,需要說的也就只有兩種情況:null
和正常數(shù)值計(jì)算,以及 null
和 null
的計(jì)算。首先明確一點(diǎn)是 null
和 null
不論是 &
還是 |
,結(jié)果都一定是 null
。這個(gè)也是符合正常邏輯的:兩個(gè)對(duì)象都表示“沒有數(shù)值”,那么結(jié)果怎么可能會(huì)變成有值呢?而一邊 null
一邊不是 null
的情況只有兩種:true
和 null
的運(yùn)算,以及 false
和 null
的運(yùn)算,于是表格就只剩下這么一點(diǎn)了:

&
運(yùn)算符要求嚴(yán)苛一點(diǎn),因?yàn)樗枰獌蓚€(gè)都 true
才能返回 true
,因此有一個(gè) null
我們肯定不會(huì)把 null
視為 true
來看,因此 true & null
是 null
。而 |
運(yùn)算符較為松散,有一個(gè) true
就行。因此,既然我有 true
了,那么我管你剩下那個(gè)是不是 null
,我有 true
不就可以了?所以 true | null
是 true
。
接著。false
和 null
的計(jì)算行為稍顯奇特,這是為了保證數(shù)學(xué)推導(dǎo)過程的嚴(yán)謹(jǐn)性。我們來使用邏輯運(yùn)算來看這個(gè)處理規(guī)則:
其中
?是且的意思,而
?就是 a 且 b 的意思,而
?就是 a 或 b 的意思。
按照這個(gè)處理規(guī)則進(jìn)行,我們可以看到我們使用了一次等價(jià)變換:德?摩根律。我們參照這個(gè)結(jié)論表達(dá)式 ?以及對(duì)偶的另外一個(gè)表達(dá)式
?來計(jì)算
false & null
以及 false | null
。
可以從推導(dǎo)計(jì)算里看到,false & null
通過德摩根律迂回了一下之后得到的結(jié)果是 false
,而 false | null
也是如此運(yùn)算,得到的結(jié)果是 null
。這就是為什么這兩個(gè)計(jì)算表達(dá)式結(jié)果會(huì)這么奇怪的原因。
除了計(jì)算公式別扭以外,使用上和正常的可空值類型是一樣的。不過這里我們就不再贅述了,因?yàn)椴僮魇且粯拥模瑳]必要說兩遍。
Part 5 判斷一個(gè)泛型參數(shù)的實(shí)際類型是否包含 null
值
這是一個(gè)好問題。既然說到了可空值類型了,那么我們就得給大家掰扯掰扯如何判斷泛型參數(shù)的實(shí)例是不是 null
,以及泛型參數(shù)的實(shí)際類型自身是否包含 null
值。
我們都知道,只有引用類型和可空值類型包含一個(gè) null
數(shù)值,但在普通的值類型里是不可能有 null
的。但是對(duì)于一個(gè)泛型參數(shù)來說,我們壓根不知道它具體是值類型還是引用類型,因此我們無從下手判斷是否一個(gè)泛型參數(shù)作為類型的實(shí)例是否為 null
。
實(shí)際上,我們可以直接用 ReferenceEquals
或 ==
來和 null
進(jìn)行比較。由于泛型參數(shù)在正常情況下是無從知道它是什么數(shù)據(jù)類型的,C# 會(huì)直接假設(shè)為 object
或者它的子類型。注意,這個(gè)假設(shè)排除掉了指針類型。但這也是前面說過的。正是因?yàn)檫@種假設(shè)的存在,所以它必然是一個(gè)派生體系上的一環(huán)。而 object
這個(gè)最終基類型里包含一個(gè) ReferenceEquals
可以判斷是否和某個(gè)實(shí)例引用的地址是相同的,因此我們可以拿這個(gè)直接去參與比較。不論是值類型還是引用類型,這個(gè)判斷 null
都是正確的??赡芤恍┵Y料或書籍上會(huì)直接告訴你,值類型不要使用 ReferenceEquals
方法來比較引用,因?yàn)樗鼈円b箱。但我們讓一個(gè)即使可能是值類型的類型實(shí)例判斷是否為 null
是可以使用它的,這是因?yàn)槟呐滤侵殿愋?,裝箱之后地址也不可能為 null
。換句話說,只要一個(gè)實(shí)例是有數(shù)值的,那么它不管裝箱與否,最終都必然有一個(gè)地址數(shù)值(值類型會(huì)因?yàn)檠b箱而得到一個(gè)地址數(shù)值,而引用類型自己則就是地址數(shù)值),但它一定不可能是 null
,畢竟它是有值的。所以,我們可以利用這一點(diǎn)判 null
。
那么,如何確認(rèn)一個(gè)泛型參數(shù) T
是可空的呢?可空類型包含兩個(gè):引用類型和可空值類型。
如代碼所示。要想判斷實(shí)例 obj
是否為 null
,我們的判斷次序是先和 null
比較,如果是,那么很顯然這個(gè)數(shù)據(jù)類型就是包含 null
的,直接返回 true
即可。
不過,如果它不是 null
,那么它就具有數(shù)值,因此我們無從知道它是不是可空的,所以需要繼續(xù)判斷。此時(shí)我們會(huì)使用反射機(jī)制獲取類型的信息。這里我們用到一個(gè) typeof(T)
語法來獲取一個(gè)泛型參數(shù) T
的類型信息。注意這里雖然是泛型參數(shù),但仍然可以使用此語法來判別,因?yàn)樗罱K會(huì)被替代為一個(gè)實(shí)際類型,那么就相當(dāng)于是把一個(gè)實(shí)際類型替代到這里。接著,我們使用其中的 IsValueType
屬性就可以知道它是否是值類型了。如果它不是值類型,就一定是引用類型或者指針類型,因此這里我們需要判斷它是不是指針類型或引用類型。使用 type.IsPointer
可以確認(rèn)它是不是指針類型。如果是則一定包含 null
(因?yàn)橹羔橆愋吞焐蜁?huì)用到 null
),因此直接返回 true
;否則它不是指針類型后,我們繼續(xù)使用 !type.IsValueType
來判斷它是不是引用類型。因?yàn)閷?duì) IsValueType
屬性的結(jié)果取反就意味著它不是值類型。不是值類型的情況只有指針類型或引用類型兩種,而指針類型前面已經(jīng)判斷過了,所以這里只剩下一種情況:它是引用類型。而引用類型也自帶 null
的情況,因此引用類型也是包含 null
的,因此也返回 true
。
最后,我們使用 Nullable
這個(gè)類型里自帶的 GetUnderlyingType
來判斷一個(gè) Nullable<T>
類型的 T
是什么。如果它是可空值類型的話,這個(gè)方法將會(huì)返回 T
類型的類型信息實(shí)例(即 Type
類型的實(shí)例)作為結(jié)果,反之會(huì)返回 null
(比如它完全就不是 T?
類型,根本無法獲取里面 T
的信息)。如果這里我們比較結(jié)果 != null
,那么很顯然的就是它一定是 T?
類型了,因此我們返回 true
即可。
最后,如果以上條件沒有一個(gè)滿足,那么就說明它是普通的值類型,因此返回 false
,因?yàn)槠胀ㄖ殿愋蜎]有 null
一說。