【轉】4、可空類型Nullable<T>究竟是什麼鬼html
首先咱們都知道引用類型默認值都是null,而值類型的默認值都有非null。ide
爲何引用類型能夠爲空?由於引用類型變量都是保存一個對象的地址引用(就像一個url對應一個頁面),而引用類型值爲null的時候是變量值指向了一個空引用(如同一個空的url)函數
那爲何值不能有空值呢?其實很簡單,由於如int值範圍是-2147483648到2147483647。其中根本就沒有給null值留那麼一個位置。this
舉個栗子吧,咱們定義一我的(Person),它有三個屬性出生日期(BeginTime)、死亡日期(EndTime)、年齡(Age)。url
若是這我的還健在人世,請問怎麼給死亡日期賦值?有人很聰明說「爲空啊」。是的,這就是咱們的需求。spa
微軟在C#2.0的時候就爲咱們引入了可null值類型( System.Nullable<T> ),那麼下面來定義Person類。3d
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 }
這樣,咱們就能夠很容易得到一我的的年齡了。code
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後面加一個問號,這兩種是等效的。多虧了微軟的語法糖。htm
咱們來看看 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(); } } }
和系統的可空類型得出了相同的結果。
有同窗問,怎麼樣才能夠作到直接賦值呢?這個我也沒有什麼好的辦法,或許須要編譯器的支持。
以上內容都是胡說八道。但願能對您有那麼一點點用處,感謝閱讀。
============== 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(); } } }
如此,咱們已經完成了自定義可空類型的直接賦值。但只是部分,若是想要賦值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(); } } }
轉換關鍵字:operator、explicit與implicit解析資料:http://www.cnblogs.com/hunts/archive/2007/01/17/operator_explicit_implicit.html
你們還能夠玩出更多的花樣!!!
本文已同步至《C#基礎知識鞏固系列》