《深入理解C#》整理2-可空類型

一、沒有值怎麼辦

以DateTime爲例,購物系統中存在發貨日期,但在下單未發貨的情況下,發貨日期應當可爲空,但編譯器是不允許DateTime變量設置爲空的。在C#2之後我們可以使用可空類型,但在C#1中又是如何處理的?

1、爲什麼值類型不能爲空

  • 對於引用變量來說,其值是一個引用;對於值類型來說,值是它本身真實的數據。非空引用值提供了訪問一個對象的途徑,然而null意味着它不引用任何對象。
  • 內存中會全用零來表示null,其本質上採用的是和其他引用一樣的方式來存儲的,引用類型的變量沒有在任何地方隱藏額外的bit,這意味着不能將全零值用於一個真正的引用。另外在那麼多的活動對象之前,內存早就用光了,這也是爲什麼null不是有效的值類型的原因

舉例來說:byte變量的值用單獨一個字節來存儲,可以將值0~255存儲到變量中,如果試圖將超出這個範圍的值存儲到其中,那麼讀取到的就是“垃圾”數據。256個“普通”值加1個null值,總共要處理257個值,沒有辦法用一個字節存儲那麼多的值。如果爲每個值類型都設置一個額外的標誌位判斷一個值是null還是一個“真正”的值,此外每次想要使用值時都得對這個標誌位進行檢查,內存的消耗將急劇增加。

2、在C#1中表示空值的模式

1、魔值

  • 第一種模式是犧牲一個值來表示空值,主要是作爲DateTime的解決方案。它有悖於我在前面給出的理由,即假設每個值都能用於一般用途。所以,我們會犧牲一個值(通常是DateTime.MinValue)來表示空值,這個值稱爲“魔值”。使用魔值的一個好處在於,它不會浪費任何內存,也不需要添加任何新的類型。然而,它要求你謹慎選擇一個合適值。一經選定,這個值將永遠不能用來表示真正的數據。另外,這個設計是不“優雅”的。

2、引用類型包裝

  • 第二個解決方案可以採取兩種形式。較簡單的形式是直接用object作爲變量類型,並根據需要進行裝箱和拆箱。較複雜的形式是假定值類型A可空,就爲它準備一個引用類型B。在引用類型B中,包含值類型A的一個實例變量。B中還聲明瞭隱式轉換操作符,允許將B轉換成A,以及將A轉換成B。然它們允許直接使用null,但都要求在堆上創建對象。所以,如果非常頻繁地使用這種方式,會造成難以進行垃圾回收。

3、額外的布爾標識

  • 最後一種模式的基本思路是使用一個普通的值類型的值,同時用另一個值(一個布爾標誌)來表示值是“真正”存在,還是應該被忽略。同樣,有兩種方式來實現這個解決方案。要麼在代碼中維護兩個單獨的變量,要麼將“值和標誌”封裝到另一個值類型中。這種方式存在相同的缺點:針對想要處理的每個值類型,都必須創建一個新的類型。另外,如果值因某種原因要進行裝箱,那麼不管它是否被認爲是空值,都要像平時那樣進行裝箱。採用封裝方式也是C#2的可空類型的工作方式。

二、System.Nullable和System.Nullable

可空類型的核心部分是System.Nullable,靜態類System.Nullable則提供了一些工具方法,可以簡化可空類型的使用。

1、Nullable簡介

  • 類型參數T有一個值類型約束,還意味着不能使用另一個可空類型作爲實參。對於任何具體的可空類型來說,T的類型稱爲可空類型的基礎類型,如Nullable的基礎類型就是int。Nullable最重要的部分就是它的屬性,即HasValue和Value。如果存在一個非可空的值,那麼Value表示的就是這個值。如果不存在真正的值,就會拋出一個InvalidOperationException。而HasValue是一個簡單的Boolean屬性,它指出是存在一個真正的值,還是應該將實例視爲null。
  • Nullable有兩個構造函數。其中,默認構造函數創建“一個沒有值的實例”。另一個構造函數則接受T的一個實例作爲值。實例一經創建,就是“不易變”的(假如一個類型的實例在創建之後便不能更改,就說這種類型是不易變的)。
  • Nullable引入了一個名爲GetValueOrDefault的新方法,它有兩個重載方法,如果實例存在值,就返回該值,否則返回一個默認值。其中一個重載方法沒有任何參數(在這種情況下會使用基礎類型的泛型默認值),另一個重載方法則允許你指定要返回的默認值。
  • Nullable實現的其他方法全都覆蓋了現有的方法:GetHashCode、ToString和Equals。GetHashCode會在實例沒有值的時候返回0;如果有值,就返回那個值的GetHashCode。ToString在沒有值的時候返回空字符串,否則返回那個值的ToString。Equals稍複雜,後面會有說明
  • 最後,框架提供了兩個轉換。首先,是T到Nullable的隱式轉換。轉換結果爲一個HasValue屬性爲true的實例。同樣,Nullable可以顯式轉換爲T,其作用與Value屬性相同,在沒有真正的值可供返回時將拋出一個異常

2、Nullable裝箱和拆箱

只有在涉及裝箱和拆箱時,CLR纔會讓可空類型有一些特殊的行爲。其他時候,可空類型使用的是“標準”的泛型、轉換、方法調用。Nullable的實例要麼裝箱成空引用(如果沒有值),要麼裝箱成T的一個已裝箱的值(如果有值)。已裝箱的值可以拆箱成普通類型,或者拆箱成對應的可空類型Nullable。拆箱一個空引用時,如果拆箱成普通類型,會拋出一個NullReferenceException;但如果拆箱成恰當的可空類型,就會拆箱成沒有值的一個實例。

3、Nullable實例的相等性

調用first.Equals(second)的具體規則如下:

  • 如果first沒有值,second爲null,它們就是相等的;
  • 如果first沒有值,second不爲null,它們就是不相等的;
  • 如果first有值,second爲null,它們就是不相等的;
  • 否則,如果first的值等於second,它們就是相等的。

這些規則與.NET其他地方的相等性規則是一致的。所以,可空實例可以作爲字典的鍵來使用。

4、來自非泛型Nullable類的支持

由於歷史原因遺留下來的Nullable類提供了3個方法,前兩個方法是比較方法:Compare和Equals;Compare使用Comparer.Default來比較兩個基礎值,Equals使用EqualityComparer.Default。對於沒有值的實例,上述每個方法返回的值都遵從.NET的約定:空值與空值相等,小於其他所有值。第三個方法爲GetUnderlyingType,如果參數是一個可空類型,方法就返回它的基礎類型;否則就返回null。

三、C# 2爲可空類型提供的語法糖

在C#語言規範中,可空類型是指可以包含空值的類型——如引用類型和Nullable。可空值類型的空值(null value)是指在“HasValue返回false”時的值。或者是“實例沒有值”時的值,它是C#特有的,CLI規範和Nullable本身的文檔都沒有提到它。

1、?修飾符

它是指定可空類型的一種快捷方式。Nullable寫成byte?就可以了,兩者可以互換使用,最終會編譯成完全相同的IL。

2、使用null進行賦值和比較

建立一個Person類,屬性有姓名、出生日期和死亡日期,如果人仍然健在,在這種情況下,死亡日期就要用null來表示。將死亡日期變量同null進行比較時,是在問它的值是否爲空值。同樣,將null作爲DateTime?實例來使用時,實際是通過調用類型的默認構造函數爲這個類型創建空值。

3、可空轉換和操作符

1、涉及可空類型的轉換

假如允許從非可空值類型(S)轉換成另一個非可空值類型(T),那麼同時允許進行以下轉換:

  • S?到T?(可能是顯式或隱式的,具體取決於原始轉換);
  • S到T?(可能是顯式或隱式的,具體取決於原始轉換);
  • S?到T(總是顯式的)

對於用戶自定義的轉換,這些涉及可空類型的額外轉換稱爲提升轉換

2、涉及可空類型的操作符

C#允許重載以下操作符:

  • 一元:+ ++ - -- ! ~ truefalse
  • 二元:+ - * / % & | ^ << >>
  • 相等:== !=
  • 關係:< > <= >=

當爲非可空的值類型T重載了上述操作符之後,可空類型T?將自動擁有相同的操作符,只是操作數和結果的類型稍有不同。這些操作符稱爲提升操作符,不管它們是預定義的操作符,還是用戶自定義的操作符。提升操作符在使用時存在着一些限制:

  • true和false操作符永遠不會被提升,這兩個操作符本身就十分少用,所以這個限制對我們的影響不大;
  • 只有操作數是非可空值類型的操作符纔會被提升;
  • 對於一元和二元操作符(相等和關係操作符除外),返回類型必須是一個非可空的值類型;
  • 對於相等和關係操作符,返回類型則必須是bool;
  • 應用於bool?的&和|操作符有單獨定義的行爲,後面會介紹

image-20201022202505528

4、可空邏輯

bool?它的值可能爲true、false或null,那意味着使用二元操作符,總共會有9種不同的組合。

image-20201022203430160

注意:本節討論的提升操作符和轉換,還有bool?邏輯,它們都是由C#編譯器提供的,而不是由CLR或者框架本身提供的。

5、對可空類型使用as操作符

在C# 2之前,as操作符只能用於引用類型。而在C# 2中,它也可以用於可空類型。其結果爲可空類型的某個值——空值(如果原始引用爲錯誤類型或空)或有意義的值。

6、空合併操作符??

這個二元操作符在對first ?? second求值時,大致會經歷以下步驟:(1) 對first進行求值;(2) 如結果非空,則該結果就是整個表達式的結果;(3) 否則求second的值,其結果作爲整個表達式的結果

它並非只能用於可空值類型,還能應用於引用類型。另外,它的結合性是右結合,這意味着表達式first ?? second ?? third實際相當於first ?? (second ?? third)。如果還有更多的操作數,可以此類推。可以使用任意數量的表達式,並依次對它們求值,遇到第一個非空的結果就停止。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章