1. 程式人生 > >[C#基礎知識系列]專題十:全面解析可空型別

[C#基礎知識系列]專題十:全面解析可空型別

引言:

  C# 2.0 中還引入了可空型別,可空型別也是值型別,只是可空型別是包括null的值型別的,下面就介紹下C#2.0中對可空型別的支援具體有哪些內容(最近一直都在思考如何來分享這篇文章的,因為剛開始覺得可空型別使用過程中比較簡單,覺得沒有講的必要,但是考慮到這個系列的完整性,決定還是嘮叨下吧,希望對一些不熟悉的人有幫助)。

一、為什麼會有可空型別

   如果朋友們看了我之前的分享,對於這一部分都不會陌生,因為我一般介紹C#特性經常會以這樣的方式開頭的, 因為每個特性都是有它出現的原因的(有一句佛語這是這麼講的:萬事皆有因,有因必有果),首先來說說這個因的(果當然是新增加了可空型別這個新特性了。),當我們在設計資料庫的時候,我們可以設定資料庫欄位允許為null值,如果資料庫欄位是日期等這樣在C#語言是值型別時,當我們把資料庫表對映一個物件時,此時Datetime型別在C# 語言中是不能為null的,如果這樣就會與資料庫的設計有所衝突,這樣開發人員就會有這樣的需求了——值型別能不能也為可空型別的

?同時微軟也看出了使用者有這樣的需求,所以微軟在C# 2.0中就新增加了一種型別——可空型別,即包含null值的值型別,這個也就是我理解的因了,介紹完因之後,當然就是好好嘮叨下可空型別是個什麼東西的了?

二、可空型別的介紹

   可空型別也是值型別,只是它是包含null的一個值型別。我們可以像下面這樣表示可空型別(相信大家都不陌生):

 int? nullable = null;

  上面程式碼 int? 就是可空的int型別(有人可能會這樣的疑問的, 如果在C#1中我硬要讓一個值型別為一個可空型別怎麼辦到呢?當然這個在C#1之前也是有可以辦到的,只是會相當麻煩,對於這個如果有興趣的朋友可以去刨下根),然而其實 "?"這個修飾符只是C#提供的一個語法糖(所謂語法糖,就是C#提供的一種方便的形式,其實肯定沒有int? 這個型別,這個int?編譯器認為的就是Nullable<int>型別,即可空型別),其實真真C# 2.0提供的可空型別是——Nullable<T>(這個T就是上專題介紹的泛型引數,其中T只能為值型別,因為從可空型別的定義為:public

 struct Nullable<T> where T : struct)和Nullable。下面給出一段程式碼來介紹可空型別的使用:

namespace 可空型別Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            // 下面程式碼也可以這樣子定義int? value=1;
            Nullable<int> value = 1;

            Console.WriteLine("可空型別有值的輸出情況:");
            Display(value);
            Console.WriteLine();
            Console.WriteLine();

            value = new Nullable<int>();
            Console.WriteLine("可空型別沒有值的輸出情況:");
            Display(value);
            Console.Read();
        }

        // 輸出方法,演示可空型別中的方法和屬性的使用
        private static void Display(int? nullable)
        {
            // HasValue 屬性代表指示可空物件是否有值
            // 在使用Value屬性時必須先判斷可空型別是否有值,
            // 如果可空型別物件的HasValue返回false時,將會引發InvalidOperationException異常
            Console.WriteLine("可空型別是否有值:{0}", nullable.HasValue);
            if (nullable.HasValue)
            {
                Console.WriteLine("值為: {0}", nullable.Value);
            }

            // GetValueOrDefault(代表如果可空物件有值,就用它的值返回,如果可空物件不包含值時,使用預設值0返回)相當與下面的語句
            // if (!nullable.HasValue)
            // {
            //    result = d.Value;
            // }

            Console.WriteLine("GetValueorDefault():{0}", nullable.GetValueOrDefault());

            // GetValueOrDefault(T)方法代表如果 HasValue 屬性為 true,則為 Value 屬性的值;否則為 defaultValue 引數值,即2。
            Console.WriteLine("GetValueorDefalut過載方法使用:{0}", nullable.GetValueOrDefault(2));

            // GetHashCode()代表如果 HasValue 屬性為 true,則為 Value 屬性返回的物件的雜湊程式碼;如果 HasValue 屬性為 false,則為零
            Console.WriteLine("GetHashCode()方法的使用:{0}", nullable.GetHashCode());
        }
    }
}

輸出結果:

上面的演示程式碼中都註釋,這裡就不再解釋了,為了讓大家明白進一步理解可空型別是值型別,下面貼出中間語言程式碼截圖:

三、空合併操作符(?? 操作符)

  ??操作符也就是"空合併操作符",它代表的意思是兩個運算元,如果左邊的數不為null時,就返回左邊的數,如果左邊的數為null,就返回右邊的數,這個操作符可以用於可空型別,也可以用於引用型別,但是不能用於值型別(之所以不能應用值型別(這裡除了可空型別),因為??運算子要對左邊的數與null進行比較,然而值型別,不能與null型別比較,所以就不支援??運算子),下面用一個例子來掩飾下??運算子的使用(??這個運算子可以方便我們設定預設值,可以避免在程式碼中寫if, else語句,簡單程式碼數量,從而有利於閱讀。)

static void Main(string[] args)
        {
            Console.WriteLine("??運算子的使用如下:");
            NullcoalescingOperator();
            Console.Read();
        }

        private static void NullcoalescingOperator()
        {
            int? nullable = null;
            int? nullhasvalue = 1;
            
            // ??和三目運算子的功能差不多的
            // 所以下面程式碼等價於:
            // x=nullable.HasValue?b.Value:12;
            int x = nullable ?? 12;

            // 此時nullhasvalue不能null,所以y的值為nullhasvalue.Value,即輸出1
            int y = nullhasvalue ?? 123;
            Console.WriteLine("可空型別沒有值的情況:{0}",x);
            Console.WriteLine("可空型別有值的情況:{0}", y);

            // 同時??運算子也可以用於引用型別, 下面是引用型別的例子
            Console.WriteLine();
            string stringnotnull = "123";
            string stringisnull = null;
           
            // 下面的程式碼等價於:
            // (stringnotnull ==null)? "456" :stringnotnull
            // 同時下面程式碼也等價於:
            // if(stringnotnull==null)
            // {
            //      return "456";
            // }
            // else
            // {
            //      return stringnotnull;
            // }
            // 從上面的等價程式碼可以看出,有了??運算子之後可以省略大量的if—else語句,這樣程式碼少了, 自然可讀性就高了
            string result = stringnotnull ?? "456";
            string result2 = stringisnull ?? "12";
            Console.WriteLine("引用型別不為null的情況:{0}", result);
            Console.WriteLine("引用型別為null的情況:{0}", result2);
        }

下面是執行結果截圖:

四、可空型別的裝箱和拆箱

   值型別存在裝箱和拆箱的過程,可空型別也屬於值型別,從而也有裝箱和拆箱的過程的, 這裡先介紹下裝箱和拆箱的概念的, 裝箱指的的從值型別到引用型別的過程,拆箱當然也就是裝箱的反過程,即從引用型別到值型別的過程(這裡進一步解釋下我理解的裝箱和拆箱,首先.Net中值型別是分配在堆疊上的,然而引用型別分配在託管堆上,裝箱過程就是把值型別的值從推棧上拷貝到託管堆上,然後推棧上儲存的是對託管堆上拷貝值的引用,然而拆箱就是把託管堆上的值拷貝到堆疊上.簡單一句話概況,裝箱和拆箱就是一個值的拷貝的一個過程,就想搬家一樣,把東西從一個地方搬到另一個地方,對於深入的理解,大家可以參考下園中的博文.), 括號中是我理解的裝箱和拆箱的過程,下面就具體介紹下可空型別的裝箱和拆箱的:

  當把一個可空型別賦給一個引用型別變數時,此時CLR 會對可空型別(Nullable<T>)物件進行裝箱處理,首先CLR會檢測可空型別是否為null,如果為null,CLR則不進行實際的裝箱操作(因為null可以直接賦給一個引用型別變數),如果不為null,CLR會從可空型別物件中獲取值,並對該值進行裝箱(這個過程就是值型別的裝箱過程了。),當把一個已裝箱的值型別賦給一個可空型別變數時,此時CLR會對已裝箱的值型別進行拆箱處理,如果已裝箱值型別的引用為null,此時CLR會把可空型別設為null(如果覺得囉嗦,大家可以直接看下面的程式碼,程式碼中也會有詳細的註釋)。下面用一個示例來演示下可空型別的裝箱和拆箱的使用,這樣可以幫助大家更好的理解前面介紹的概念:

static void Main(string[] args)
        {
            //Console.WriteLine("??運算子的使用如下:");
            //NullcoalescingOperator();
            Console.WriteLine("可空型別的裝箱和拆箱的使用如下:");
            BoxedandUnboxed();
            Console.Read();
        }

        // 可空型別裝箱和拆箱的演示
        private static void BoxedandUnboxed()
        {
            // 定義一個可空型別物件nullable
            Nullable<int> nullable = 5;
            int? nullablewithoutvalue = null;

            // 獲得可空物件的型別,此時返回的是System.Int32,而不是System.Nullable<System.Int32>,這點大家要特別注意下的
            Console.WriteLine("獲取不為null的可空型別的型別為:{0}",nullable.GetType());

            // 對於一個為null的型別呼叫方法時出現異常,所以一般對於引用型別的呼叫方法前,最好養成習慣先檢測下它是否為null
            //Console.WriteLine("獲取為null的可空型別的型別為:{0}", nullablewithoutvalue.GetType());

            // 將可空型別物件賦給引用型別obj,此時會發生裝箱操作,大家可以通過IL中的boxed 來證明
            object obj = nullable;

            // 獲得裝箱後引用型別的型別,此時輸出的仍然是System.Int32,而不是System.Nullable<System.Int32>
            Console.WriteLine("獲得裝箱後obj 的型別:{0}", obj.GetType());

            // 拆箱成非可空變數
            int value = (int)obj;
            Console.WriteLine("拆箱成非可空變數的情況為:{0}", value);

            // 拆箱成可空變數
            nullable = (int?)obj;
            Console.WriteLine("拆箱成可空變數的情況為:{0}", nullable);

            // 裝箱一個沒有值的可空型別的物件
            obj = nullablewithoutvalue;
            Console.WriteLine("對null的可空型別裝箱後obj 是否為null:{0}", obj==null);

            // 拆箱成非可空變數,此時會丟擲NullReferenceException異常,因為沒有值的可空型別裝箱後obj等於null,引用一個空地址
            // 相當於拆箱後把null值賦給一個int 型別的變數,此時當然就會出現錯誤了
            //value = (int)obj;
            //Console.WriteLine("一個沒有值的可空型別裝箱後,拆箱成非可空變數的情況為:{0}", value);

           // 拆箱成可空變數
            nullable = (int?)obj;
            Console.WriteLine("一個沒有值的可空型別裝箱後,拆箱成可空變數是否為null:{0}", nullable == null);
        }

執行結果:

   上面程式碼中都有註釋的, 而且程式碼也比較簡單, 這裡就不解釋了, 其實可空型別的裝箱和拆箱操作大家可以就理解為非可空值型別的裝箱和拆箱的過程,只是對於非可空型別因為包含null值,所以CLR會提前對它進行檢查下它是否為空,為null就不不任何處理,如果不為null,就按照非可空值型別的裝箱和拆箱的過程來裝箱和拆箱。

五、小結

   到這裡本專題的介紹就完成了,本專題主要介紹了下可空型別以及可空型別相關的知識,希望這篇文章可以幫助大家對可空型別的認識可以更加全面,下一個專題將和大家介紹下匿名方法, 匿名方法也是Lambda表示式和Linq的一個鋪墊,然而它是C#2中被提出來了的, 從而可以看出Lambda和Linq在C# 3.0中被新增其實是微軟早在C# 2.0的時候就計劃好了的,早就計劃好了的(這也是我的推斷,然而我覺得為什麼它不直接在把Lambda和Linq都放在C# 2中提出來的, 卻偏偏放在C# 3.0中提出,我理解原因有——1 覺得微軟當時肯定是想一起提出的,但是後面發現這幾個新的特性提出後會對編譯器做比較大的改動,需要比較長的時間來實現,此時又怕使用者等不及了,覺得C#很多東西都沒有,所以微軟就先把做好了的部分先發布出來,然而把Lambda和Linq放到C#3來提出。我推理覺得應該是這樣的,所以C#的所有特性都是緊密相連的。)

  注意:有網友提醒了我一個需要主要的點,所以放在這裡補充下,如果細心的朋友可能會發現,當可空型別為null時,此時還是可以呼叫HasValue屬性,即此時的返回值為false,可能就會有這樣的疑問的,為什麼物件為null了還可以呼叫屬性,此時不會出現NullReferenceException異常嗎?其實對於這個問題我之前也覺得奇怪的,後面通過查詢也知道了原因了——首先,可空型別是值型別,當可空型別為null時,此時可空型別並不是null(引用型別中的null),對於可空型別null這個是一個有效的值型別的,所以它呼叫HasValue不會丟擲異常的(值型別時不可能為null的,可空型別為null的,此時null與引用型別是不一樣的,這點大家必須明確)。同時這個問題也使我加深了對可空型別的理解,這裡分享出來可以讓大家進一步理解可空型別,如果大家有什麼意見和C#特性需要注意的地方歡迎大家給我留言。