我們知道,一個值類型的變量永遠不可能為null。它總是包含值類型本身。遺憾的是,這在某些情況下會成為問題。例如,設計一個數據庫時,可將一個列定義成為一個32位的整數,并映射到FCL的Int32數據類型。但是,數據庫中的一個列可能允許值為空;用Microsoft .NET Framework處理數據庫可能變得相當困難,因為在CLR中,沒有辦法將一個Int32值表示為null。 Microsoft ADO.NET的表適配器確實支持可空類型。但遺憾的是,System.Data.SqlType命名空間中的值類型沒有用可空類型替換,部分原因是類型之間沒有"一對一"的對應關系。例如,SqlDecimal類型對最大允許38位數,而普通的Decimal類型最大只允許29位數。 還有一個例子:在java中,java.util.Date類是一個引用類型,所以該類型的一個變量能設為null。但在CLR中,System.DateTime是一個值類型,一個DateTime變量永遠都不能設為null。如果用java寫的一個應用程序想和運行CLR的一個web服務交流日期/時間,那么一旦java發送了一個null,就會出問題,因為CLR不知道如何表示null,不知道如何操作它。 為了解決這個問題,Microsoft在CLR中引入了可空值類型(nullable value type)的概念。為了理解它們是如何工作的,先看一看System.Nullable<T>類。他是在FCL中定義的。以下是System.Nullable<T>類型定義的邏輯表示:
[Serializable][DebuggerStepThrough]public struct Nullable<T> where T : struct{ #region Sync with runtime code //下面兩個字段表示狀態 internal T value; internal bool has_value; #endregion public Nullable(T value) { this.has_value = true; this.value = value; } public bool HasValue { get { return has_value; } } public T Value { get { if (!has_value) throw new InvalidOperationException("Nullable object must have a value."); return value; } } public override bool Equals(object other) { if (other == null) return has_value == false; if (!(other is Nullable<T>)) return false; return Equals((Nullable<T>)other); } bool Equals(Nullable<T> other) { if (other.has_value != has_value) return false; if (has_value == false) return true; return other.value.Equals(value); } public override int GetHashCode() { if (!has_value) return 0; return value.GetHashCode(); } public T GetValueOrDefault() { return value; } public T GetValueOrDefault(T defaultValue) { return has_value ? value : defaultValue; } public override string ToString() { if (has_value) return value.ToString(); else return String.Empty; } public static implicit operator Nullable<T>(T value) { return new Nullable<T>(value); } public static explicit operator T(Nullable<T> value) { return value.Value; } // // These are called by the JIT // #PRagma warning disable 169 // // JIT implementation of box valuetype System.Nullable`1<T> // static object Box(T? o) { if (!o.has_value) return null; return o.value; } static T? Unbox(object o) { if (o == null) return null; return (T)o; } #pragma warning restore 169}可以看出,這個類封裝了也可以為null的一個值類型的表示。由于Nullable<T>本身是一個值類型,所以它的實例仍然是"輕量級"的。也就是說,實例仍然在棧上,而且一個實例的大小就是原始值類型的大小加上一個Boolean字段的大小。注意,Nullable的類型參數T被約束為struct。這是由于引用類型的變量已經可以為null,所以沒必要再去照顧它。.
現在,如果想要在代碼中使用一個可空的Int32,就可以向下面這樣寫:
Nullable<Int32> x = 5;Nullable<Int32> y = null;Console.WriteLine("x: HasValue={0}, Value={1}",x.HasValue, x.Value);Console.WriteLine("y: HasValue={0}, Value={1}",y.HasValue, y.GetValueOrDefault());輸出的結果為:
x: HasValue=True, Value=5y: HasValue=False, Value=0 一、C#對可空值類型的支持 注意,C#允許在代碼中使用簡單的語法來初始化上述兩個Nullable<Int32>變量x和y。事實上,C#開發團隊希望將可空值類型集成到C#語言中,是它們成為"一等公民"。為此,C#提供了一個更清晰的語法來處理可空值類型。C#允許用問號表示法來聲明并初始化x和y變量:
Int32? x =5;Int32? y =null;
在C#中,Int32等價于Nullable<Int32>。但是,C#在此基礎上更進一步,允許開發人員在可空實例上進行轉換和轉型。C#還允許開發人員向可空實例類型應用操作符。以下代碼對此進行了演示;
private static void ConversionsAndCasting(){ // 從可空的 Int32 轉換為 Nullable<Int32> Int32? a = 5; // 從'null'隱式轉換為 Nullable<Int32> Int32? b = null; // 從 Nullable<Int32> 顯示轉換為 Int32 Int32 c = (Int32)a; // 在可空基類型之間的轉型 Double? d = 5; // Int32 轉型到 Double? (d是double類型 值為5) Double? e = b; // Int32? 轉型到 Double? (e為 null)}C#還允許向可空實例應用操作符。下面是一些例子:
private static void Operators(){ Int32? a = 5; Int32? b = null; // 一元操作符 (+ ++ - -- ! ~) a++; // a = 6 b = -b; // b = null // 二元操作符 (+ - * / % & | ^ << >>) a = a + 3; // a = 9 b = b * 3; // b = null; // 相等性操作符 (== !=) if (a == null) { /* no */ } else { /* yes */ } if (b == null) { /* yes */ } else { /* no */ } if (a != b) { /* yes */ } else { /* no */ } // 比較操作符 (<, >, <=, >=) if (a < b) { /* no */ } else { /* yes */ }}下面總結了C#如何解析操作符:
* 一元操作符 操作符是null,結果也是null * 二元操作符 兩個操作符中任何一個是null,結果就是null。但有一個例外,它發生在將&和|操作符應用于Boolean?操作數的時候。在這種情況下,這兩個操作符的行為和SQL的三值邏輯一樣的。對于這兩個操作符,如果兩個操作符都不是null,那么操作符和平常一樣工作。如果兩個操作符都是null,結果就是null。特殊情況就是其中之一為null時發生。下面列出了針對操作符的各種true,false和null組合:
| 操作符→ | true | false | null |
| 操作符↓ | |||
| true | & = true| = true | & = false| = true | & = null| = true |
| false | & = false | = true | & = false| = false | & = false| = null |
| null | & = null| = true | & = false| = null | & = null| = null |
* 相等性操作 兩個操作符都是null,兩者相等。一個操作符為null,則兩個不相等。兩個操作數都不是null,就比較值來判斷是否相等。 * 關系操作符 兩個操作符任何一個是null,結果就是false。兩個操作數都不是null,就比較值。
應該注意的是,操作符實例時會生成大量代碼。例如以下方法:
private static Int32? NullableCodeSize(Int32? a, Int32? b) { return (a + b);}在編譯這個方法時,會生成相當多的IL代碼,而且會使對可空類型的操作符慢于非可控類型執行的同樣的操作。編譯器生成的代碼等價于以下C#代碼:
private static Nullable<Int32> NullableCodeSize( Nullable<Int32> a, Nullable<Int32> b) { Nullable<Int32> nullable1 = a; Nullable<Int32> nullable2 = b; if (!(nullable1.HasValue & nullable2.HasValue)){ return new Nullable<Int32>();} return new Nullable<Int32>(nullable1.GetValueOrDefault() + nullable2.GetValueOrDefault());}19.2 C#的空結合操作符 C#提供了一個所謂的"空結合操作符",即??操作符,它要獲取兩個操作符。假如左邊的操作符不為null,就返回操作符這個操作符的值。如果左邊的操作符為null,就返回右邊的操作符的值。利用空接合操作符,可方便地設置的默認值。 空接合操作符的一個妙處在于,它既能用于引用類型也能用于可空值類型。以下代碼演示了如何使用??操作符:
private static void NullCoalescingOperator() { Int32? b = null; // 下面這行等價于: // x = (b.HasValue) ? b.Value : 123 Int32 x = b ?? 123; Console.WriteLine(x); // "123" // 下面這行等價于: // String temp = GetFilename(); // filename = (temp != null) ? temp : "Untitled"; String filename = GetFilename() ?? "Untitled";}有人爭辯說??操作符不過是?:操作符的"語法糖"而已,所以C#團隊不應該將這個操作符添加到語言中。實際上,??提供了重大的語法上的改進。
第一個改進是??操作符能更好的支持表達式:
Func<String> f = () => SomeMethod ?? "Untitled";
相比下一行代碼,上述代碼更容易容易閱讀和理解。下面這行代碼要求進行變量賦值,而且用一個語句還搞不定:
Func<String> f = () => { var temp = SomeMethod(); return temp !=null ?temp : "Untitled"; }第二個改進就是??在符合情形下更好用。例如,下面這行代碼:
String s = SomeMethod() ?? SomeMethod2 ?? "Untitled";
它比下面這一堆代碼更容易理解和閱讀:
String s;var sm1 = SomeMethod();if (sm1 != null) s = sm1;else {var sm2 = SomeMethod2();if (sm2 !=null) s = sm2;elses = "Untitled";}三、CLR對可空值類型的特殊支持 1.可空值類型的裝箱 先假定有一個為null的Nullable<Int32>變量。如果將該變量傳給一個期待獲取一個Object的方法,那么該變量必須裝箱,并將對已裝箱的Nullable<Int32>的一個引用傳給方法。但是,這在邏輯上講不通,因為現在向方法傳遞的一個非null的值——而Nullable<Int32>變量邏輯上包含的是null值。為了解決這個問題,CLR會在裝箱一個可空變量時執行一些特殊代碼。 具體地說,當CLR對一個Nullable<T>實例進行裝箱時,會檢查它是否為null。如果是CLR不實際裝箱任何內容,并返回null。如果可空類型實例不為null,CLR從可空實例中取出值,并對其進行裝箱。也就是說,一個值為5的Nullable<Int32>會裝箱成為值為5的一個已裝箱的Int32。以下代碼對這一行進行了演示:
private static void Boxing() { // 對Nullable<T>進行裝箱,要么返回null,要么返回一個已裝箱的T Int32? n = null; Object o = n; // o 為 null Console.WriteLine("o is null={0}", o == null); // "True" n = 5; o = n; // o 引用一個已裝箱的Int32 Console.WriteLine("o's type={0}", o.GetType()); // "System.Int32"}其實在第一節中的Nullable<T>源碼中已有顯示,如:
static object Box(T? o){ if (!o.has_value) return null; return o.value;}2. 可空值類型的拆箱 CLR允許將一個已裝箱的值類型T拆箱為一個T或者一個Nullable<T>。如果對已裝箱值類型的引用是null,而且要把它拆箱為一個Nullable<T>,那么CLR會將Nullable<T>的值設為null。以下代碼進行了演示:
private static void Unboxing() { // 創建一個已裝箱的In
新聞熱點
疑難解答