1. 程式人生 > >重溫CLR(十四) 可空類型

重溫CLR(十四) 可空類型

nal int 比較 filename 校驗器 double 改進 ati .get

  我們知道,一個值類型的變量永遠不可能為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
  //下面兩個字段表示狀態
  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=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(十四) 可空類型