重溫CLR(十四) 可空類型
我們知道,一個值類型的變量永遠不可能為null。它總是包含值類型本身。遺憾的是,這在某些情況下會成為問題。例如,設計一個數據庫時,可將一個列定義成為一個32位的整數,並映射到FCL的Int32數據類型。但是,數據庫中的一個列可能允許值為空;用Microsoft .NET Framework處理數據庫可能變得相當困難,因為在CLR中,沒有辦法將一個Int32值表示為null。
還有一個例子:在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 //下面兩個字段表示狀態 internalT 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=5 y: 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組合:
相等性操作
兩個操作符都是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()); }
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; else s = "Untitled"; }
clr對可空值類型的特殊支持
clr內建對可空類型的支持。這個特殊的支持是針對裝箱、拆箱、調用getType和調用接口方法提供的,它使可控類型能無縫地集成到clr中,而且使它們具有更自然的行為,更符合大多數開發人員的預期。
可空值類型的裝箱
先假定有一個為null的Nullable<Int32>變量。如果將該變量傳給一個期待獲取一個Object的方法,那麽該變量必須裝箱,並將對已裝箱的Nullable<Int32>的引用傳給方法。但對表面上為null的值進行裝箱不符合直覺—即使Nullable<Int32>變量本身非null,它只是在邏輯上包含了null。為了解決這個問題,clr會在裝箱可空變量時執行一些特殊代碼,從表面上維持可空類型一等公民低位。
具體地說,當CLR對一個Nullable<T>實例進行裝箱時,會檢查它是否為null。如果是CLR不實際裝箱任何內容,直接返回null。如果可空類型實例不為null,CLR從可空實例中取出值,並對其進行裝箱。也就是說,一個值為5的Nullable<Int32>會裝箱成為值為5的一個已裝箱的Int32。以下代碼對這一行進行了演示:
// 對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; }
可空值類型的拆箱
CLR允許將一個已裝箱的值類型T拆箱為一個T或者一個Nullable<T>。如果對已裝箱值類型的引用是null,而且要把它拆箱為一個Nullable<T>,那麽CLR會將Nullable<T>的值設為null。以下代碼進行了演示:
// 創建一個已裝箱的Int32 Object o = 5; // 把它拆箱為一個 Nullable<Int32> 和一個 Int32 Int32? a = (Int32?)o; // a = 5 Int32 b = (Int32)o; // b = 5 // 創建初始化為null的一個引用 o = null; // 把它"拆箱"為一個Nullable<Int32> 和一個 Int32 a = (Int32?)o; // a = null b = (Int32) o; // NullReferenceException
同樣的,在第一節中的Nullable<T>源碼中已有顯示,如:
static T? Unbox(object o) { if (o == null) return null; return (T)o; }
通過可空值類型調用GetType
在一個Nullable<T>對象上調用GetType時,CLR實際上會"撒謊"說類型是T,而不是Nullable<T>。以下代碼演示了這一行為:
Int32? x = 5; // 下面會顯示"System.Int32"而不是"System.Nullable<Int32>" Console.WriteLine(x.GetType());
如果設為null,會報錯
Int32? x = null; // 下面會顯示"System.Int32"而不是"System.Nullable<Int32>" Console.WriteLine(x.GetType());
輸出結果
Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object.
at System.Object.GetType()
at AttributeStudy2.Program.Main() in E:\project\2Core\Test\ClrStudy\AttributeStudy2\Program.cs:line 19
通過可空值類型調用接口方法
在下面代碼中,將一個Nullable<Int32>類型的變量n轉型為一個接口類型IComparable<Int32>。然而,Nullable<T>不像Int32那樣實現了IComparable<Int32>接口。C#編譯器允許這樣的代碼通過編譯,而且CLR的校驗器也會認為這樣的代碼是可驗證的,從而允許我們使用一種更簡潔的語法:
Int32? n = 5; Int32 result = ((IComparable<Int32>)n).CompareTo(5); // 能順利通過編譯和允許 Console.WriteLine(result); // 0
假如CLR沒有提供這一特殊支持,那麽為了在一個可空值類型上調用接口方法,就要寫非常繁瑣的代碼。首先必須轉型對已拆箱的值類型,然後才能轉型為接口以發出調用:
result = ((IComparable)(Int32)n).CompareTo(5); // 這太繁瑣了 Console.WriteLine(result); // 0
重溫CLR(十四) 可空類型