四、可空類型Nullable 到底是什么鬼


值類型為什么不可以為空

首先我們都知道引用類型默認值都是null,而值類型的默認值都有非null。

為什么引用類型可以為空?因為引用類型變量都是保存一個對象的地址引用(就像一個url對應一個頁面),而引用類型值為null的時候是變量值指向了一個空引用(如同一個空的url

那為什么值不能有空值呢?其實很簡單,因為如int值范圍是-2147483648到2147483647。其中根本就沒有給null值留那么一個位置。

我們為什么需要用到可空類型

舉個栗子吧,我們定義一個人(Person),它有三個屬性出生日期(BeginTime)、死亡日期(EndTime)、年齡(Age)。

如果這個人還健在人世,請問怎么給死亡日期賦值?有人很聰明說“為空啊”。是的,這就是我們的需求。

微軟在C#2.0的時候就為我們引入了可null值類型( System.Nullable<T> ),那么下面來定義Person類。

 1 public class Person
 2 {
 3     /// <summary>
 4     /// 出生日期
 5     /// </summary>
 6     public DateTime BeginTime { get; set; }
 7     /// <summary>
 8     /// 死亡日期
 9     /// </summary>
10     public System.Nullable<DateTime> EndTiem { get; set; }
11     public int Age
12     {
13         get
14         {
15             if (EndTiem.HasValue)//如果掛了(如果有值,證明死了)
16             {
17                 return (EndTiem.Value - BeginTime).Days;
18             }
19             else//還沒掛
20             {
21                 return (DateTime.Now - BeginTime).Days;
22             }
23         }
24     }
25 }

 

這樣,我們就可以很容易獲得一個人的年齡了。

static void Main(string[] args)
{
    Person p1 = new Person()
    {
        BeginTime = DateTime.Parse("1990-07-19")
    };

    Person p2 = new Person()
    {
        BeginTime = DateTime.Parse("1893-12-26"),
        EndTiem = DateTime.Parse("1976-09-09")
    };

    Console.WriteLine("我今年" + p1.Age + "歲。");
    Console.WriteLine("毛爺爺活了" + p2.Age + "歲。");

    Console.ReadKey();
}

可空類型的實現

我們前面用到了 System.Nullable<DateTime> 來表示可空時間類型,其實平時我們用得更多的是 DateTime? 直接在類型T后面加一個問號,這兩種是等效的。多虧了微軟的語法糖。

我們來看看 System.Nullable<T> 到底是何物。

搜噶,原來是一個結構。還看到了我們屬性的 HasValue和Value屬性。原來竟這般簡單。一個結構兩個屬性,一個存值,一個存是否有值。那么下面我們也來試試吧。

不好意思,讓大家失望了。前面我們就說過了,值類型是不可以賦值null的(結構也是值類型)。

怎么辦!怎么辦!不對啊,微軟自己也是定義的結構,它怎么可以直接賦值null呢。(奇怪,奇怪,畢竟是人家微軟自己搞得,可能得到了特殊的待遇吧)

可是,這樣就讓我們止步了嗎?NO!我們都知道,看微軟的IL(中間語言)的時候,就像脫了它的衣服一樣,很多時候不明白的地方都可以看個究竟,下面我們就去脫衣服。

首先,我們用幾種不同的方式給可空類型賦值。

static void Main(string[] args)
{

    System.Nullable<int> number1 = null;

    System.Nullable<int> number2 = new System.Nullable<int>();

    System.Nullable<int> number3 = 23;

    System.Nullable<int> number4 = new System.Nullable<int>(88);

    Console.ReadKey();
}    

 

然后用reflector看編譯后的IL。

原來如此,可空類型的賦值直接等效於構造實例。賦null時其實就是調用空構造函數,有值時就就把值傳入帶參數的構造函數。(柳暗花明又一村。如此,我們是否可以接着上面截圖中的 MyNullable<T> 繼續模擬可空類型呢?且繼續往下看。)

public struct MyNullable<T> where T : struct
{
    //錯誤    1    結構不能包含顯式的無參數構造函數 
    //還好 bool默認值就是false,所以這里不顯示為 this._hasValue = false也不會有影響
    //public MyNullable()
    //{
    //    this._hasValue = false;
    //}
    public MyNullable(T value)//有參構造函數
    {
        this._hasValue = true;
        this._value = value;
    }

    private bool _hasValue;

    public bool HasValue//是否不為空
    {
        get { return _hasValue; }
    }

    private T _value;
    public T Value//
    {
        get
        {
            if (!this._hasValue)//如沒有值,還訪問就拋出異常
            {
                throw new Exception(" 可為空的對象必須具有一個值");
            }
            return _value;
        }
    }
}

 

喲西,基本上已經模擬出了可空類型出來的。(但是我們還是不能直接賦值,只能通過構造函數的方式來使用自定義的可空類型)。

全部代碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 可空類型
{
    public class Person
    {
        /// <summary>
        /// 出生日期
        /// </summary>
        public DateTime BeginTime { get; set; }
        /// <summary>
        /// 死亡日期
        /// </summary>
        public MyNullable<DateTime> EndTiem { get; set; } //這里改用MyNullable
        /// <summary>
        /// 年齡
        /// </summary>
        public double Age
        {
            get
            {
                if (EndTiem.HasValue)//如果掛了(如果有值,證明死了)
                {
                    return (EndTiem.Value - BeginTime).Days / 365;
                }
                else//還沒掛
                {
                    return (DateTime.Now - BeginTime).Days / 365;
                }
            }
        }
    }

    public struct MyNullable<T> where T : struct
    {
        //錯誤    1    結構不能包含顯式的無參數構造函數 
        //還好 bool默認值就是false,所以這里不顯示為 this._hasValue = false也不會有影響
        //public MyNullable()
        //{
        //    this._hasValue = false;
        //}
        public MyNullable(T value)//有參構造函數
        {
            this._hasValue = true;
            this._value = value;
        }

        private bool _hasValue;

        public bool HasValue//是否不為空
        {
            get { return _hasValue; }
        }

        private T _value;
        public T Value//
        {
            get
            {
                if (!this._hasValue)//如沒有值,還訪問就拋出異常
                {
                    throw new Exception(" 可為空的對象必須具有一個值");
                }
                return _value;
            }
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Person p1 = new Person()
            {
                BeginTime = DateTime.Parse("1990-07-19")
            };

            Person p2 = new Person()
            {
                BeginTime = DateTime.Parse("1893-12-26"),
                EndTiem = new MyNullable<DateTime>(DateTime.Parse("1976-09-09"))//這里使用MyNullable的有參構造函數
            };

            Console.WriteLine("我今年" + p1.Age + "歲。");
            Console.WriteLine("毛爺爺活了" + p2.Age + "歲。");

            Console.ReadKey();
        }

    }
}
View Code

 

和系統的可空類型得出了相同的結果。

總結

  • 可空類型是結構(也就是值類型)
  • 所以可空類型的null值和引用類型的null是不一樣的。(可空類型的並不是引用類型的null,而是用結構的另一種表示方式來表示null

 

有同學問,怎么樣才可以做到直接賦值呢?這個我也沒有什么好的辦法,或許需要編譯器的支持。

以上內容都是胡說八道。希望能對您有那么一點點用處,感謝閱讀。

(首發鏈接:http://www.cnblogs.com/zhaopei/p/5537759.html )

 

 


 

============== 2016-06-05更新==============

上面我們提出了疑問“怎么樣才可以做到直接賦值呢”,本來我是沒有好的解決辦法。這里要感謝我們的園友@沖殺給我提供了好的解決方案。

implicit(關鍵字用於聲明隱式的用戶定義類型轉換運算符。

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

只需要在 struct MyNullable<T> 中添加以上代碼,就可以直接賦值了。(作用等效於是直接重寫了“=”賦值符號

完整代碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace test
{
    public class Person
    {
        /// <summary>
        /// 出生日期
        /// </summary>
        public DateTime BeginTime { get; set; }
        /// <summary>
        /// 死亡日期
        /// </summary>
        public MyNullable<DateTime> EndTiem { get; set; } //這里改用MyNullable
        /// <summary>
        /// 年齡
        /// </summary>
        public double Age
        {
            get
            {
                if (EndTiem.HasValue)//如果掛了(如果有值,證明死了)
                {
                    return (EndTiem.Value - BeginTime).Days / 365;
                }
                else//還沒掛
                {
                    return (DateTime.Now - BeginTime).Days / 365;
                }
            }
        }
    }

    public struct MyNullable<T> where T : struct
    {
        //錯誤    1    結構不能包含顯式的無參數構造函數 
        //還好 bool默認值就是false,所以這里不顯示為 this._hasValue = false也不會有影響
        //public MyNullable()
        //{
        //    this._hasValue = false;
        //} 

        public MyNullable(T value)//有參構造函數
        {
            this._hasValue = true;
            this._value = value;
        }

        private bool _hasValue;

        public bool HasValue//是否不為空
        {
            get { return _hasValue; }
        }

        private T _value;
        public T Value//
        {
            get
            {
                if (!this._hasValue)//如沒有值,還訪問就拋出異常
                {
                    throw new InvalidOperationException(" 可為空的對象必須具有一個值");
                }
                return _value;
            }
        }

        public static implicit operator MyNullable<T>(T value)
        {
            return new MyNullable<T>(value);
        } 
    }
    class Program
    {
        static void Main(string[] args)
        {
            Person p1 = new Person()
            {
                BeginTime = DateTime.Parse("1990-07-19")
            };

            Person p2 = new Person()
            {
                BeginTime = DateTime.Parse("1893-12-26"),
                EndTiem = DateTime.Parse("1976-09-09") 
                //new MyNullable<DateTime>(DateTime.Parse("1976-09-09"))
                //這里使用MyNullable的有參構造函數
            };

            Console.WriteLine("我今年" + p1.Age + "歲。");
            Console.WriteLine("毛爺爺活了" + p2.Age + "歲。"); 

            Console.ReadKey();
        }

    }
}
View Code

 

如此,我們已經完成了自定義可空類型的直接賦值。但只是部分,如果想要賦值null呢?

同樣還是出現了最開始的編譯錯誤。我們想到既然上面的值賦值可以重新(隱式轉換),那null應該也可以啊(null是引用類型的一個特定值)。

再加一個重載:

//隱式轉換
public static implicit operator MyNullable<T>(string value)
{
    if (value == null)
        return new MyNullable<T>();
    throw new Exception("賦值右邊不能為字符串");
    //這里不知道是否可以在編譯期間拋出錯誤(或者怎樣限制只能傳null)
}

 

如此可以滿足我們的需求了(並無異常)。

可惜美中不足,如果給 p2.EndTiem 賦值一個非空字符串時,要運行時才會報錯(而系統的可空類型會在編譯期就報錯)。不知道大神們可有解!!

雖然如此,能做到直接賦值還是讓我小小激動了一把。為此,特意查了下關鍵字 implicit operator ,又是讓我小小激動了一把,我們不僅可以“重寫”賦值,我們還可以“重寫”+ - * / % & | ^ << >> == != > < >= <=等運算符。

下面我們先來“重寫”下自定義可空類型的比較(==)運算符。

//"重寫"比較運算符
public static bool operator ==(MyNullable<T> operand, MyNullable<T> operand2)
{
    if (!operand.HasValue && !operand2.HasValue)
    {
        return true;
    }
    else if (operand.HasValue && operand2.HasValue)
    {
        if (operand2.Value.Equals(operand.Value))
        {
            return true;
        }
    }
    return false;
}

//"重寫"比較運算符
public static bool operator !=(MyNullable<T> operand, MyNullable<T> operand2)
{
    return !(operand == operand2);
}

 

Console.WriteLine("p1.EndTiem == null," + (p1.EndTiem == null).ToString());
Console.WriteLine("p2.EndTiem == null," + (p2.EndTiem == null).ToString());
Console.WriteLine("p1.EndTiem == DateTime.Parse(1976-09-09)," + (p1.EndTiem == DateTime.Parse("1976-09-09")).ToString());
Console.WriteLine("p2.EndTiem == DateTime.Parse(1976-09-09)," + (p2.EndTiem == DateTime.Parse("1976-09-09")).ToString());

p1.EndTiem = DateTime.Parse("2016-06-06");
p2.EndTiem = null;
Console.WriteLine();
Console.WriteLine("賦值 p1.EndTiem = DateTime.Parse(2016-06-06)  p2.EndTiem = null 后:");
Console.WriteLine("p1.EndTiem == null," + (p1.EndTiem == null).ToString());
Console.WriteLine("p2.EndTiem == null," + (p2.EndTiem == null).ToString());
Console.WriteLine("p1.EndTiem == DateTime.Parse(2016-06-06)," + (p1.EndTiem == DateTime.Parse("2016-06-06")).ToString());
Console.WriteLine("p2.EndTiem == DateTime.Parse(2016-06-06)," + (p2.EndTiem == DateTime.Parse("2016-06-06")).ToString());

結果完全符合!

完整代碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace test
{
    public class Person
    {
        /// <summary>
        /// 出生日期
        /// </summary>
        public DateTime BeginTime { get; set; }
        /// <summary>
        /// 死亡日期
        /// </summary>
        public MyNullable<DateTime> EndTiem { get; set; } //這里改用MyNullable
        /// <summary>
        /// 年齡
        /// </summary>
        public double Age
        {
            get
            {
                if (EndTiem.HasValue)//如果掛了(如果有值,證明死了)
                {
                    return (EndTiem.Value - BeginTime).Days / 365;
                }
                else//還沒掛
                {
                    return (DateTime.Now - BeginTime).Days / 365;
                }
            }
        }
    }

    public struct MyNullable<T> where T : struct
    {
        //錯誤    1    結構不能包含顯式的無參數構造函數 
        //還好 bool默認值就是false,所以這里不顯示為 this._hasValue = false也不會有影響
        //public MyNullable()
        //{
        //    this._hasValue = false;
        //} 

        public MyNullable(T value)//有參構造函數
        {
            this._hasValue = true;
            this._value = value;
        }

        private bool _hasValue;

        public bool HasValue//是否不為空
        {
            get { return _hasValue; }
        }

        private T _value;
        public T Value//
        {
            get
            {
                if (!this._hasValue)//如沒有值,還訪問就拋出異常
                {
                    throw new InvalidOperationException(" 可為空的對象必須具有一個值");
                }
                return _value;
            }
        }

        //隱式轉換
        public static implicit operator MyNullable<T>(T value)
        {
            return new MyNullable<T>(value);
        }

        //隱式轉換
        public static implicit operator MyNullable<T>(string value)
        {
            if (value == null)
                return new MyNullable<T>();
            throw new Exception("賦值右邊不能為字符串");
            //這里不知道是否可以在編譯期間拋出錯誤(或者怎樣限制只能傳null)
        }

        //"重寫"比較運算符
        public static bool operator ==(MyNullable<T> operand, MyNullable<T> operand2)
        {
            if (!operand.HasValue && !operand2.HasValue)
            {
                return true;
            }
            else if (operand.HasValue && operand2.HasValue)
            {
                if (operand2.Value.Equals(operand.Value))
                {
                    return true;
                }
            }
            return false;
        }

        //"重寫"比較運算符
        public static bool operator !=(MyNullable<T> operand, MyNullable<T> operand2)
        {
            return !(operand == operand2);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Person p1 = new Person()
            {
                BeginTime = DateTime.Parse("1990-07-19")
            };

            Person p2 = new Person()
            {
                BeginTime = DateTime.Parse("1893-12-26"),
                EndTiem = DateTime.Parse("1976-09-09")
                //new MyNullable<DateTime>(DateTime.Parse("1976-09-09"))
                //這里使用MyNullable的有參構造函數
            };

            Console.WriteLine("我今年" + p1.Age + "歲。");
            Console.WriteLine("毛爺爺活了" + p2.Age + "歲。");
            Console.WriteLine();

            Console.WriteLine("p1.EndTiem == null," + (p1.EndTiem == null).ToString());
            Console.WriteLine("p2.EndTiem == null," + (p2.EndTiem == null).ToString());
            Console.WriteLine("p1.EndTiem == DateTime.Parse(1976-09-09)," + (p1.EndTiem == DateTime.Parse("1976-09-09")).ToString());
            Console.WriteLine("p2.EndTiem == DateTime.Parse(1976-09-09)," + (p2.EndTiem == DateTime.Parse("1976-09-09")).ToString());

            p1.EndTiem = DateTime.Parse("2016-06-06");
            p2.EndTiem = null;
            Console.WriteLine();
            Console.WriteLine("賦值 p1.EndTiem = DateTime.Parse(2016-06-06)  p2.EndTiem = null 后:");
            Console.WriteLine("p1.EndTiem == null," + (p1.EndTiem == null).ToString());
            Console.WriteLine("p2.EndTiem == null," + (p2.EndTiem == null).ToString());
            Console.WriteLine("p1.EndTiem == DateTime.Parse(2016-06-06)," + (p1.EndTiem == DateTime.Parse("2016-06-06")).ToString());
            Console.WriteLine("p2.EndTiem == DateTime.Parse(2016-06-06)," + (p2.EndTiem == DateTime.Parse("2016-06-06")).ToString());     

            Console.ReadKey();
        }

    }
}
View Code

 

 

轉換關鍵字:operator、explicit與implicit解析資料:http://www.cnblogs.com/hunts/archive/2007/01/17/operator_explicit_implicit.html

大家還可以玩出更多的花樣!!!

 

本文已同步至《C#基礎知識鞏固系列


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM