探究 Nullable泛型結構

System.Nullable<T> 結構是個還滿有趣的玩意,一則因為它是個實值型別、二則因為它多載了 implicit 和 explicit  這兩個運算子,因此造就了它的有趣程度倍增。

(1)  Nullable<T> 基本介紹

Nullable<T> 用意是在讓本來沒有 null (在 Visual Basic 稱為 Nothing) 特性的實值型別能夠像參考型別一樣可以指派 null 或比較其是否為 null。.Net Framework 自 2.0 版開始有 Nullbale<T>,事實上泛型也是,所以就不要拿甚麼 .Net Framework 1.1/1.0 來跟我說嘴了。

Nullable<T> 的基本宣告是長成這樣 (參考 MSDN 文件 Nullable<T> 結構 )

[SerializableAttribute]
public struct Nullable<T>
where T : struct

Nullable<T> 有兩個重要的唯讀屬性:HasValue Property 與 Value Property。

先來談談 HasValue, HasValue 這個 Property 所關聯的 Field 決定了一個 Nullable<T> 型別的變數究竟是不是 null (正確的來說這個變數本身有沒有假裝自己是 null ,因為實值型別事實上是不應該指向 null 的),所以呢,當某個 Nullable<T> 的 HasValue 屬性值是 false 的時候,和 null 的比較運算就會傳回 true。

基本上 HasValue Property  內部實作看起來像這樣 (是『像』,不是完全一樣喔)

        private bool hasValue;
        public bool HasValue
        {
            get
            {
                return this.hasValue;
            }
        }

那 Value Property 呢?當你宣告了一個 Nullable<T> 的變數, 比方是 Nullable<System.Int32> 好了,這時和 Value Property 的 getter 運算子所處理的那個 Filed(欄位) 就會塞進一個 System.Int32 的初始值。 既然它基本上一定會被塞進一個實值型別的值,為什麼當你的程式碼是像以下這樣寫的時候卻會出錯呢?

    class Program
    {
        static void Main(string[] args)
        {
            Nullable<int> x = null;
            int y = x.Value;
            Console.ReadLine();
        }
    }

執行偵錯結果圖:

這個出錯的原因就在於 Value Property 內部實作內容會先檢查 hasValue Field,Value Property 內部實作看起來像是這樣:

        internal T value;
        public T Value
        {  get
            {
                if (!this.hasValue)
                {
                    // 這邊會 throw 出一個  InvalidOperationException (詳細不寫了)
                }
                return this.value;
            }
        }

各位應該看得出來,由於 Nullable<T> 的泛型參數是被約束成 struct,意即這個 value Field 一定會塞一個東西進來,而且絕不會是參考型別式的 null (請記得 Nullable<T> 的 null 是一種偽裝),但是 Nullable<T> 為了善盡其偽裝成 null 的任務,於是在 Value Property 的 getter 裡做了對於 hasValue Filed 的檢查。

介紹完這兩個 Property,可以發現它們都只有 getter 而沒有 setter,這也就是為什麼唯讀的原因。

(2) Nullable<T> 的建構式

接著我們來看看 Nullable<T> 的建構式大約是甚麼樣子。

        private bool hasValue;
        internal T value;

        public Nullable(T value)
        {
            this.value = value;
            this.hasValue = true;
        }

請注意,結構型別是無法使用 C# 或 VB 為它加入一個無參數建構式的,可能有人覺得奇怪,那為什麼可以像以下這樣寫?
 

   Nullable<int> x = new Nullable<int>(); 

簡單說明就是當你這樣寫的時候,編譯後的程式碼呼叫的並不是建構式,而是一個稱為 initobj 的指令 (這個指令會讓將T 型別的預設值指派給 value Field,而 hasValue Field 則會被設定成 false) 。

我們用以下的程式碼來說明 Nullable<T> 的建構式運作 (有點狗尾續貂)

        static void Main(string[] args)
        {
            Nullable<int> y = new Nullable<int>(10);
        }

a.首先會先宣告一個 Nullable<int> 型別的變數,其名稱為 y。
b.接著執行 Nullable<int> 建構函式,所以會把 10 指派給 value Filed,接著將 hasValue Field 設定為 true。

(3)  implicit 和 explicit 運算子

implict (也稱為隱含轉換) 的作用是將 T 轉換為 Nullable<T>(我一直覺得轉換這字眼有點奇妙,明明是變成兩個東西,不過習慣上都是這樣講就不用深究了) ,這個多載看起來大概是這樣:
 

        public static implicit operator Nullable<T> (T value)
        {
            return new Nullable<T>(value);
        }

explicit (也稱明確轉換) 的作用是將 Nullable<T> 轉換為 T,它看起來大概是這樣
 

        public static explicit operator T(Nullable<T> value)
        {
            return value.Value;
        }

接著用以下簡單的程式碼來展示這兩個轉換運算子的行為:
 

       static void Main(string[] args)
        {
            int x = 800;
            Nullable<int> y = x;
            int z =(int)y;
        }

a. 首先我們有一個 int 型別的變數 x ,並賦予它一個值 800。
b. 在 Nullable<int> y = x; 這一行 Nullable<int> 型別的變數在等號的左邊,int 型別的變數則在右邊,所以會呼叫上述的 implict operator,於是這一行的程式碼就會呼叫 Nullable<int> 的建構式。所以以下兩行程式碼式幾乎等價的。
        

    Nullable<int> y = x;
    Nullable<int> y = new Nullable<int>(x);


回頭看一下前述關於建構式的說明,它會把 x 的值 800 指派給 y 的 value Field,同時把 hasValue Field 設定為 true;
c.在 int z =(int)y; 則是 int 型別的變數在等號左邊,Nullable<int> 型別的變數在右邊並且呼叫明確轉換為 int ,這時就會用到 explicit operator,所以它就會直接呼叫 y 的 Value Property,回頭看一下前述關於 Value Property 的說明,它會執行 Value Property 的 getter,先判斷 hasValue Field 是否為 true,在我們的例子裡是 true,接著就會 return value Filed 的值,也就 800,並且將這個 800 指派給 z 變數。


(4) GetValueOrDefault() Method

這個 Method 有兩個多載,在此只講無參數的那一個,內部實作大概是這樣:

       public T GetValueOrDefault()
        {
            return this.value;
        }

和 Value Property 的 getter 很類似,差異是它不會先判斷 hasValue 是否為 true 就會直接回傳 value Filed 的值。這個 Method 在 Nullable<T> 的比較運算上有很重要的地位。

(5) Nullable<T> 與 T 的比較運算

我們用以下的程式碼來解釋這個比較運算的流程:

            Nullable<int> x = 100;
            int y = 999;
            if (x > y)
            { }

在   if (x > y) 的行為流程是先呼叫 x 的 GetValueOrDefault() 將取得的結果值與 y 比較,再結合 x 的 HasValue Property 的返回值決定為 true 或 false (意即只要 hasValue Field 是 false,結果一定是 false)。

(6) Nullable<T> 與 Nullable<T> 的比較運算
            Nullable<int> x = 100;
            Nullable<int> y = 999;
            if (x > y)
            { }

和前述的行為很類似,但因為兩個都是 Nullable<int> 所以會執行個別的 GetValueOrDefault() 後先比較這兩個值,再結合 x 與 y 的 HasValue Property 的返回值決定為 true 或 false。

意即只要 x 與 y  的 hasValue Field 做 AND 運算結果是 false,雖然GetValueOrDefault() 的比較是 true ,結果一定是 false;另一個情形是 x 與 y  的 hasValue Field 都是 false,此時不論 x 或 y 的 GetValueOrDefault()  一定會相同,那結果就會回傳 true 。也就是說它們的比較方式是類似這樣的:

        static private bool Do(int? x, int? y)
        {
            bool result = (x.GetValueOrDefault() > y.GetValueOrDefault()) && (x.HasValue == y.HasValue);
            return result;
        }

 

(7) Nullable<T> 與 null 的比較運算

在 C# 上可以使用以下兩種寫法比較 Nullable<T> 與  null,一是直接比較 null ,另一個則是使用 HasValue Property,例如

            Nullable<int> x = 100;
            if (x == null) 
            { }
            Nullable<int> x = 100;
            if (x.HasValue !=true) 
            { }

以上兩種方式在一般情形幾乎是等效的 (Demo 發現在某個情境下會有些許不同),都是呼叫 HasValue Property 的 getter 來決定是否為 null。

在 Visual Basic 則可以這樣寫

        Dim x As Nullable(Of Integer) = 100
        If x Is Nothing Then

        End If
        Dim x As Nullable(Of Integer) = 100
        If x.HasValue <> True Then

        End If

特別為 Visual Basic 使用者強調一件事情,請不要用 IsNothing Function 來判斷 Nullable<T> 是否為 Nothing (null),有位仁兄在某個論壇上說使用IsNothing Function 判斷才是通用的方式,但我覺得他完全搞錯了, IsNothing Function 的傳入參數型別是 object,只要有普通基本程度的人都知道把實值型別的物件傳到使用 object 型別作為參數的 Method 裡會有甚麼後果,這種行為一定會導致 Boxing 作業產生,而 Boxing 會導致效能的耗損,雖然難免會遇到非 Boxing 不可的情形,但在這篇文章講述的情境是絕對可以避免的。

另外,非不得已不要使用 Nullable<T>.Equals Method,理由差不多,這個 Method 的傳入參數型別是 object,會產生 Boxing 作業。

最後,為什麼我都是用 Nullable<T> 而不用 T?,這兩個宣告其實效果相同,因為怕在文章中 T 和 T? 分不清,所以才會全面寫成 Nullable<T>。寫完這篇文章的感想是『潮水退了,才知道誰沒穿褲子;看了程式碼,才知道誰不懂 Boxing/Unboxing 』