匹夫細說C#:可以為null的值類型,詳解可空值類型


首先祝大家中秋佳節快樂~

0x00 前言

眾所周知的一點是C#語言是一種強調類型的語言,而C#作為Unity3D中的游戲腳本主流語言,在我們的開發工作中能夠駕馭好它的這個特點便十分重要。事實上,怎么強調C#的這個特點都不為過,因為它牽涉到編程的很多方面。一個很好的例子便是我們本文要介紹的內容——可空型,它是因何出現的,而它的出現又有什么意義呢?以及如何在Unity3D游戲的開發中使用它呢?那么就請各位讀者朋友帶着這些疑問,通過下面的文字來尋找這些問題的答案吧。

0x01 如果沒有值?

一個了解一點C#基礎知識的人都知道,值類型的變量永遠不會為null,因為值類型的值是其本身。而對於一個引用類型的變量來說,它的值則是對一個對象的引用。那么空引用是表示一個值呢,還是表示沒有值呢?如果表示沒有值,那么沒有值可以算是一種有效的值嗎?如果我們根據相關標准中關於引用類型的定義,我們其實很容易就可以發現,一個非空的引用值事實上提供了訪問一個對象的途徑,而空引用(null)當然也表示一個值,只不過它是一個特殊的值,即意味着該變量沒有引用任何對象。但null在本質上和其他的引用的處理方式是一樣的,通過相同的方式在內存中存儲,只不過內存會全部使用0來表示null,因為這種操作的開銷最低,僅僅需要將一塊內存清除,這也是為何所有的引用類型的實例默認值都是null的原因。

但是,正如在本節一開始說的,值類型的值永遠不能是null,但是在我們的開發工作中是否會恰巧遇到一個必須讓值類型變量的值既不是負數也不是0,而是真正的不存在的情況呢?答案是是的,很常見。

一種最常見的情況是在設計數據庫時,是允許將一列的數據類型定義為一個32位整數,同時映射到C#中的Int32這個數據類型。但是,數據庫中的一列值中是存在為空的可能性的,換言之在該列的某一行上的有可能是沒有任何值的,即不是0也不是負無窮,而是實實在在的空。這樣會帶來很多的隱患,也使得C#在處理數據庫時變得十分困難,原因上文已經提到過了,在C#中無法將值類型表示為空。

當然還有很多種可能的情況,例如在開發手機游戲時需要通過移動手指來滑動選擇一塊區域內的游戲單位,一次拖動完成之后,顯然應該將本次拖動的數據清空,以作為開始下一次拖動的開始條件,而往往這些拖動數據在Unity3D的腳本語言中都是作為值類型出現的,因而無法直接設為空,所以也會給開發帶來些許不便。

那么如果沒有一個可以讓值類型直接表示空的方法出現,我們是否還有別的手段來實現類似的功能呢?下面我們就來聊聊如果沒有可空類型,應該如何在邏輯上近似實現值類型表示空的功能。

0x02 表示空值的一些方案

假設如果真的沒有一種可以直接表示空值的方案出現,那么我們是否能想到一些替代方案呢?所以本節就歸納一下三種用來表示空值的方案。

1.使用魔值

首先我們要知道值類型的值都是它本身,換言之每個值我們都希望是有意義的。而魔值這個概念或者說方案的出現,恰恰是違背這一原則的,即放棄一個有意義的值,並且使用它來表示空值,這個值在我們的邏輯中與別的值不同,這便是魔值。因為它讓一個有意義的值消失了,例如魔值選為-1000,那么-1000這個值便不再表示-1000了,相反,它意味着空。

回到剛剛的例子中,在數據庫中如果有映射成Int32類型的某列值中恰好有一個是空,那么我們可以選擇(犧牲)一個恰當的值來表示空。這樣做的好處在於不會浪費內存,同樣也不需要定義新的類型。但犧牲哪個值來作為魔值便成為了一個需要慎重考慮的事情。因為一旦做出選擇,就意味着一個有意義的值的消失。

當然,使用魔值這種方案在實際的開發中也顯得很low,這是因為問題並沒有被真正的解決,相反,我們只是耍了一個小聰明來實現暫時蒙混過關。因此我並不十分喜歡這種方案。

2 使用標志位

如果我們不想浪費或者說犧牲掉一個有意義的值來讓它作為魔值來表示空的話,那么只用一個值類型的實例是不夠的。這時候我們能想到的一個解決方案就是使用額外的bool型變量作為一個標識,來判定對應的值類型實例是否是空值。這種方案具體操作起來有很多種方式,例如我們可以保留兩個實例,一個是表示我們所需的普通的值的變量,另一個則是標識它是否為空值的bool類型的變量。如下面這段代碼所示:

//使用bool型變量作為標識

using UnityEngine;

using System;

using System.Collections.Generic;

 

public class Example : MonoBehaviour {

  private float _realValue;

  private bool _nullFlag;

 

  private void Update()

  {

    this._realValue = Time.time;

    this._nullFlag = false;

    this.PrintNum(this._realValue);

  }

 

  private void LateUpdate()

  {

    this._nullFlag = true;

    this.PrintNum(this._realValue);

  }

 

  // Use this for initialization

  private void Start () {

 

  }

 

  private void PrintNum(float number)

  {

    if(this._nullFlag)

    {

      Debug.Log("傳入的數字為空值");

        return;

      }

      Debug.Log("傳入的數字為:" + number); 

  }

}

在這段代碼中,我們維護了兩個變量,分別是float型的_ realValue,用來表示我們所需的值和bool型的_nullFlag,用來標識此時_ realValue所代表的值是否為空值(當然_ realValue本身不可能為空)。

這種使用額外標識的方法要比上一小節中介紹的魔值方案好一些,因為沒有犧牲任何有意義的值。但同時維護兩個變量,而且這兩個變量的關聯性很強,因此稍有不慎可能就會造成bug,那么除了同時維護兩個變量之外,還有別的具體方案可以用來實現標識是否為空值這個需求的嗎?答案是有的,一個自然而然的想法便是使用結構將這兩個值類型封裝到一個新的值類型中。我們為這個新的值類型取名為NullableValueStruct。下面我們來看看NullableValueStruct值類型的定義:

//值類型NullableValueStruct的定義

using System;

using System.Collections;

using System.Collections.Generic;

public struct NullableValueStruct

{

  private float _realValue;

  private bool _nullFlag;

 

  public NullableValueStruct(float value, bool isNull)

  {

    this._realValue = value;

    this._nullFlag = isNull

  }

 

  public float Value

  {

    get

    {

      return this._realValue;

    }

    set

    {

      this._realValue = value;

    }

  }

 

  public bool IsNull

  {

    get

    {

      return this._nullFlag;

    }

    set

    {

      this._nullFlag = value;

    }

  }

}

這樣,我們就將剛剛要單獨維護的兩個變量封裝到了一個新的類型中。而且由於這個新的類型是struct,換言之它是一個值類型因此也無需擔心會產生裝箱和拆箱的操作。下面我們就通過一段代碼,在我們游戲中使用一下這個新的值類型吧。

using UnityEngine;

using System;

using System.Collections.Generic;

 

public class Example : MonoBehaviour {

  private NullableValueStruct number = new NullableValueStruct(0f, false);

 

  private void Update()

  {

    this.number.Value = Time.time;

    this.number.IsNull = false;

    this.PrintNum(this.number);

  }

 

  private void LateUpdate()

  {

    this.number.IsNull = true;

    this.PrintNum(this.number);

  }

 

  // Use this for initialization

  private void Start () {

 

  }

 

  private void PrintNum(NullableValueStruct number)

  {

    if(number.IsNull)

    {

      Debug.Log("傳入的數字為空值");

        return;

      }

      Debug.Log("傳入的數字為:" + number.Value); 

  }

}

當然除了這種方式,是否還有別的方案呢?下面我們就來總結一下另一種方案,即使用引用類型來輔助值類型表示空值。

3 借助引用類型來表示值類型的空值

介紹完前兩種為值類型表示空值的方案之后,我們接下來再介紹最后一種方案。當然聰明的讀者朋友一定也想到了,既然值類型不能夠是null,而引用類型卻可以是null,那么是否可以借助引用類型來輔助值類型表示null呢?事實上使用引用類型來幫助表示值類型的空值,是一個很好的方向,而具體而言又可以分成兩種解決思路。

如我們所知,C#語言中的所有類型(引用類型和值類型)都是自System.Object類派生而來,雖然值類型不能為null,但是System.Object類卻可以為null,因此在所有使用值類型同時有可能需要值類型表示空值的地方使用System.Object類來代替,便可以直接使用null來表示空值了。下面讓我們來看一個小例子:

using UnityEngine;

using System;

using System.Collections.Generic;

 

public class Example : MonoBehaviour {

 

 

  private void Update()

  {

    this.PrintNum(Time.time);

  }

 

  // Use this for initialization

  private void Start () {

   

  }

 

  private void PrintNum(object number)

  {

    if(number == null)

    {

      Debug.Log("傳入的數字為空值");

      return;

    }

    float realNumber = (float)number;

    Debug.Log("傳入的數字為:" + realNumber);

  }

}

當然,使用這種方式由於會頻繁的在引用類型(System.Object)和值類型直接轉換,因此會涉及到十分頻繁的裝箱和拆箱的操作進而產生很多垃圾而引發垃圾回收機制,會對游戲的性能產生一些影響。那么是否還有別的方案,不需要涉及到頻繁的裝箱和拆箱操作呢?答案是直接使用引用類型來表示值類型,即將值類型封裝成一個引用類型。

當然,這么做之后,我們相當於重新創建了一個全新的類型,在這里我們假設我們創建的這個新的類型叫做NullableValueType(當然它事實上是引用類型),在NullableValueType類的內部保留一個值類型的實例,該值類型的實例的值便是此時NullableValueType類所表示的值,而當需要表示空值時,只需要讓NullableValueType類的實例為null即可。下面就讓我們通過代碼來定義一下NullableValueType類吧。

// NullableValueType類定義

using System;

using System.Collections;

using System.Collections.Generic;

public class NullableValueType

{

  private float _value;

 

  public NullableValueType(float value)

  {

    this._value = value;

  }

 

  public float Value

  {

    get

    {

      return this._value;

    }

    set

    {

      this._value = value;

    }

  }

}

這樣我們就將一個值類型(float)封裝成了一個引用類型,所以理論上我們既可以使用引用類型的null來表示空值,也可以借助這個類內部的值類型實例來表示有意義的值。下面我們就使用這種封裝的方式來重新實現一下上面的例子。

using UnityEngine;

using System;

using System.Collections.Generic;

 

public class Example : MonoBehaviour {

  private NullableValueType value;

 

 

  private void Update()

  {

    this.value.Value = Time.time;

    this.PrintNum(this.value);

  }

 

  // Use this for initialization

  private void Start () {

    this.value = new NullableValueType(0f);

  }

 

  private void PrintNum(NullableValueType number)

  {

    if(number == null)

    {

      Debug.Log("傳入的數字為空值");

      return;

    }

    Debug.Log("傳入的數字為:" + number.Value);

  }

}

如剛剛所說的,在這里我們可以直接判斷傳入的值是否為null來確定要表達的值是否為空值,如果不是空值,則可以利用類中封裝的值類型實例來表示它所要表達的值。這樣做的優點是無需進行引用類型和值類型之間的轉換,換言之能夠緩解裝箱和拆箱操作的頻率,減少垃圾的產生速度。但是缺點同樣十分明顯,使用引用類型對值類型進行封裝,本質上是重新定義了一個新的類型,因而代碼量將會增加同時增加維護的成本。

0x03 使用可空值類型

通過上一節的內容,我們可以發現我們自己用來解決值類型的空值問題的方案都存在着這樣或者是那樣的問題。因此,為了解決這個問題,C#引入了可空值類型的概念。在介紹究竟應該如何使用可空值類型之前,讓我們先來看看在基礎類庫中定義的結構——System.Nullable<T>。以下代碼便是System.Nullable<T>的定義:

using System;

 

namespace System

{

    using System.Globalization;

    using System.Reflection;

    using System.Collections.Generic;

    using System.Runtime;

    using System.Runtime.CompilerServices;

    using System.Security;

    using System.Diagnostics.Contracts;

 

 

    [TypeDependencyAttribute("System.Collections.Generic.NullableComparer`1")]

    [TypeDependencyAttribute("System.Collections.Generic.NullableEqualityComparer`1")]

    [Serializable]

    public struct Nullable<T> where T : struct

    {

        private bool hasValue;

        internal T value;

 

#if !FEATURE_CORECLR

        [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]

#endif

        public Nullable(T value) {

            this.value = value;

            this.hasValue = true;

        }

 

        public bool HasValue {

            get {

                return hasValue;

                }

            }

 

        public T Value {

#if !FEATURE_CORECLR

            [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]

#endif

            get {

                if (!HasValue) {

                    ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_NoValue);

                }

                return value;

            }

        }

 

#if !FEATURE_CORECLR

        [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]

#endif

        public T GetValueOrDefault() {

            return value;

        }

 

        public T GetValueOrDefault(T defaultValue) {

            return HasValue ? value : defaultValue;

        }

 

        public override bool Equals(object other) {

            if (!HasValue) return other == null;

            if (other == null) return false;

            return value.Equals(other);

        }

 

        public override int GetHashCode() {

            return HasValue ? value.GetHashCode() : 0;

        }

 

        public override string ToString() {

            return HasValue ? value.ToString() : "";

        }

 

        public static implicit operator Nullable<T>(T value) {

            return new Nullable<T>(value);

        }

 

        public static explicit operator T(Nullable<T> value) {

            return value.Value;

        }

 

                 }

 

        [System.Runtime.InteropServices.ComVisible(true)]

    public static class Nullable

    {

    [System.Runtime.InteropServices.ComVisible(true)]

        public static int Compare<T>(Nullable<T> n1, Nullable<T> n2) where T : struct

        {

            if (n1.HasValue) {

                if (n2.HasValue) return Comparer<T>.Default.Compare(n1.value, n2.value);

                return 1;

            }

            if (n2.HasValue) return -1;

                return 0;

            }

 

        [System.Runtime.InteropServices.ComVisible(true)]

        public static bool Equals<T>(Nullable<T> n1, Nullable<T> n2) where T : struct

        {

            if (n1.HasValue) {

                if (n2.HasValue) return EqualityComparer<T>.Default.Equals(n1.value, n2.value);

                return false;

                }

            if (n2.HasValue) return false;

                    return true;

                }

 

        // If the type provided is not a Nullable Type, return null.

        // Otherwise, returns the underlying type of the Nullable type

        public static Type GetUnderlyingType(Type nullableType) {

            if((object)nullableType == null) {

                throw new ArgumentNullException("nullableType");

            }

            Contract.EndContractBlock();

            Type result = null;

            if( nullableType.IsGenericType && !nullableType.IsGenericTypeDefinition) {

                // instantiated generic type only

                Type genericType = nullableType.GetGenericTypeDefinition();

                if( Object.ReferenceEquals(genericType, typeof(Nullable<>))) {

                    result = nullableType.GetGenericArguments()[0];

                }

            }

            return result;

        }

    }

}

通過System.Nullable<T>結構的定義,我們可以看到該結構可以表示為null的值類型。這是由於System.Nullable<T>本身便是值類型,所以它的實例同樣不是分配在堆上而是分配在棧上的“輕量級”實例,更重要的是該實例的大小與原始值類型基本一致,少有的一點不同便是System.Nullable<T>結構多了一個bool型字段。如果我們在進一步的觀察,可以發現System.Nullable的類型參數T被約束為結構struct,換言之System.Nullable無需考慮引用類型情況。這是由於引用類型的變量本身便可以是null。

下面我們就通過一個小例子,來使用一下可空值類型吧。

using UnityEngine;

using System;

using System.Collections;

 

public class NullableTest : MonoBehaviour {

 

    // Use this for initialization

    void Start () {

        Nullable<Int32> testInt = 999;

        Nullable<Int32> testNull = null;

        Debug.Log("testInt has value :" + testInt.HasValue);

        Debug.Log("testInt  value :" + testInt.Value);

               Debug.Log("testInt  value :" + (Int32)testInt);

        Debug.Log("testNull has value :" + testNull.HasValue);

        Debug.Log("testNull value :" + testNull.GetValueOrDefault());

    }

   

    // Update is called once per frame

    void Update () {

   

    }

}

運行這個游戲腳本,我們可以在Unity3D的調試窗口看到輸出如下的內容:

testInt has value :True

UnityEngine.Debug:Log(Object)

testInt  value :999

UnityEngine.Debug:Log(Object)

testNull has value :False

UnityEngine.Debug:Log(Object)

testNull value :0

UnityEngine.Debug:Log(Object)

讓我們來對這個游戲腳本中的代碼進行一下分析,首先我們可以發現上面的代碼中存在兩個轉換。第一個轉換發生在T到Nullable<T>的隱式轉換。轉換之后,Nullable<T>的實例中HasValue這個屬性被設置為true,而Value這個屬性的值便是T的值。第二個轉換發生在Nullable<T>顯式地轉換為T,這個操作和直接訪問實例的Value屬性有相同的效果,需要注意的是在沒有真正的值可供返回時會拋出一個異常。為了避免這個情況的發生,我們看到Nullable<T>還引入了一個方法名為GetValueOrDefault的方法,當Nullable<T>的實例存在值時,會返回該值;當Nullable<T>的實例不存在值時,會返回一個默認值。該方法存在兩個重載方法,其中一個重載方法不需要任何參數,第二種重載方法則可以指定要返回的默認值。

0x04 可空值類型的簡化語法

雖然C#引入了可空值類型的概念大大的方便了我們在表示值類型為空的情況時邏輯,但是如果僅僅能夠使用上面的例子中的那種形式,又似乎顯得有些繁瑣。好在C#還允許使用相當簡單的語法來初始化剛剛例子中的兩個System.Nullable<T>的變量testInt和testNull,這么做背后的目的是C#的開發團隊的初衷是將可空值類型集成在C#語言中。因此我們可以使用相當簡單和更加清晰的語法來處理可空值類型,即C#允許使用問號“?”來聲明並初始化上例中的兩個變量testInt和testNull,因此上例可以變成這樣:

using UnityEngine;

using System;

using System.Collections;

 

public class NullableTest : MonoBehaviour {

 

    // Use this for initialization

    void Start () {

        Int32? testInt = 999;

        Int32? testNull = null;

        Debug.Log("testInt has value :" + testInt.HasValue);

        Debug.Log("testInt  value :" + testInt.Value);

        Debug.Log("testNull has value :" + testNull.HasValue);

        Debug.Log("testNull value :" + testNull.GetValueOrDefault());

    }

   

    // Update is called once per frame

    void Update () {

   

    }

}

其中Int32?是Nullable<Int32>的簡化語法,它們之間互相等同於彼此。

除此之外,在上一節的末尾我也提到過的一點是我們可以在C#語言中對可空值類型的實例執行轉換和轉型的操作,下面我們通過一個小例子再為各位讀者加深一下印象。

using UnityEngine;

using System;

using System.Collections;

 

public class NullableTest : MonoBehaviour {

 

    // Use this for initialization

    void Start () {

        //從正常的不可空的值類型int隱式轉換為Nullable<Int32>

        Int32? testInt = 999;

        //從null隱式轉換為Nullable<Int32>

        Int32? testNull = null;

        //從Nullable<Int32>顯式轉換為不可空的值類型Int32

        Int32 intValue = (Int32) testInt;

 

    }

   

    // Update is called once per frame

    void Update () {

   

    }

}

除此之外,C#語言還允許可空值類型的實例使用操作符。具體的例子,可以參考下面的代碼:

using UnityEngine;

using System;

using System.Collections;

 

public class NullableTest : MonoBehaviour {

 

    // Use this for initialization

    void Start () {

        Int32? testInt = 999;

        Int32? testNull = null;

 

        //一元操作符 (+ ++ - -- ! ~)

        testInt ++;

        testNull = -testNull;

 

        //二元操作符 (+ - * / % & | ^ << >>)

        testInt = testInt + 1000;

        testNull = testNull * 1000;

 

        //相等性操作符 (== !=)

        if(testInt != null)

        {

            Debug.Log("testInt is not Null!");

        }

        if(testNull == null)

        {

            Debug.Log("testNull is Null!");

        }

 

        //比較操作符 (< > <= >=)

        if(testInt > testNull)

        {

            Debug.Log("testInt larger than testNull!");

        }

 

 

    }

   

    // Update is called once per frame

    void Update () {

   

    }

}

那么C#語言到底是如何來解析這些操作符的呢?下面我們來對C#解析操作符來做一個總結。

對一元操作符,包括“+”、“++”、“-”、“--”、“!”、“~”而言,如果操作數是null,則結果便是null。

對於二元操作符,包括了“+”、“-”、“*”、“/”、“%”、“&”、“|”、“^”、“<<”、“>>”來說,如果兩個操作數之中有一個為null,則結果便是null。

對於相等操作符,包括“==”、“!=”,當兩個操作數都是null,則兩者相等。如果只有一個操作數是null,則兩者不相等。若兩者都不是null,就需要通過比較值來判斷是否相等。

最后是關系操作符,其中包括了“<”“>”“<=”“>=”,如果兩個操作數之中任何一個是null,結果為false。如果兩個操作數都不是null,就需要比較值。

那么C#對可空值類型是否還有更多的簡化語法糖呢?例如在編程中常見的三元操作:表達式boolean-exp ? value0 : value1 中,如果“布爾表達式”的結果為true,就計算“value0”,而且這個計算結果也就是操作符最終產生的值。如果“布爾表達式”的結果為false,就計算“value1”,同樣,它的結果也就成為了操作符最終產生的值。答案是yes。C#為我們提供了一個“??”操作符,被稱為“空接合操作符”。“??”操作符會獲取兩個操作數,左邊的操作數如果不是null,那么返回的值是左邊這個操作數的值;如果左邊的操作數是null,便返回右邊這個操作數的值。而空接合操作符“??”的出現,為變量設置默認值提供了便捷的語法。同時,需要各位讀者注意的一點是,空接合操作符“??”既可以用於引用類型,也可以用於可空值類型,但它並非C#為可空值類型簡單的提供的語法糖,與此相反,空接合操作符“??”提供了很大的語法上的改進。下面的代碼將演示如何正確的使用可空接操作符“??”:

using UnityEngine;

using System;

using System.Collections;

 

public class NullableTest : MonoBehaviour {

 

       // Use this for initialization

       void Start () {

              Int32? testNull = null;

              //這行代碼等價於:

              //testInt = (testNull.HasValue) ? testNull.Value : 999;

              Int32? testInt = testNull ?? 999;

              Debug.Log("testInt has value :" + testInt.HasValue);

              Debug.Log("testInt  value :" + testInt.Value);

              Debug.Log("testNull has value :" + testNull.HasValue);

              Debug.Log("testNull value :" + testNull.GetValueOrDefault());

       }

      

       // Update is called once per frame

       void Update () {

      

       }

}

將這個游戲腳本加載進入游戲場景中,運行游戲我們可以看到在Unity3D編輯器的調試窗口輸出了和之前相同的內容。

當然,前文已經說過,空接合操作符“??”事實上提供了很大的語法上的改進,那么都包括哪些方面呢?首先便是“??”操作符能夠更好地支持表達式了,例如我們要獲取一個游戲中的英雄的名稱,當獲取不到正確的英雄名稱時,則需要使用默認的英雄的名稱。下面這段代碼演示了在這種情況下使用??操作符:

Func<string> heroName = GetHeroName() ?? "DefaultHeroName";

string GetHeroName()

{

       //TODO

}

當然,如果不使用??操作符而僅僅通過lambda表達式來解決同樣的需求就變得十分繁瑣了。有可能需要對變量進行賦值,同時還需要不止一行代碼:

Func<string> heroName = () => { var tempName = GetHeroName();

       return tempName != null ? tempName : "DefaultHeroName";

}

string GetHeroName()

{

       //TODO

}

相比之下,我們似乎應該慶幸C#語言的開發團隊為我們提供的??操作符。

除了能夠對表達式提供更好的支持之外,空接合操作符“??”還簡化了復合情景中的代碼,假設我們的游戲單位包括了英雄和士兵這兩種類型,如果我們需要獲取游戲單位的名稱,需要分別去查詢這兩個種類的名稱,如果查詢結果都不是可用的單位名稱,則返回默認的單位名稱,在這種復合操作中使用“??”操作符的代碼如下:

string unitName = GetHeroName() ?? GetSoldierName ?? "DefaultUnitName";

string GetHeroName()

{

       //TODO

}

string GetSoldierName()

{

       //TODO

}

如果沒有空接連接符“??”的出現,實現以上的復合邏輯則需要用比較繁瑣的代碼來完成,如下面這段代碼所示:

string unitName = String.Empty;

string heroName = GetHeroName();

if(tempName != null)

{

       unitName = tempName;

}

else

{

       string soldierName = GetSoldierName();

       if(soldierName != null)

       {

              unitName = soldierName;

       }

       else

       {

              unitName = "DefaultUnitName";

       }

}

 

string GetHeroName()

{

       //TODO

}

string GetSoldierName()

{

       //TODO

}

可見,空接合操作符不僅僅是簡單的三元操作的簡化語法糖,而是在語法邏輯上進行了重大的改進之后的產物。值得慶幸的是,不僅僅是引用類型可以使用它,我們本章的主角可空值類型同樣可以使用它。

那么是否還有之前專門供引用類型使用,而現在有了可空值類型之后,也可以被可空值類型使用的操作符呢?是有的,下面我們就再來介紹一個操作符,這個操作符在引入可空值類型之前是專門供引用類型使用的,而隨着可空值類型的出現,它也可以作用於可空值類型。它就是“as”操作符。

在C#2之前,as操作符只能作用於引用類型,而在C#2中,它也可以作用於可空值類型。因為可空值類型為值類型引入了空值的概念,因此符合“as”操作符的需求——它的結果可以是可空值類型的某個值,包括空值也包括有意義的值。

下面我們可以通過一個小例子來看看如何在代碼中將“as”操作符作用於可空值類型的實例吧。

using UnityEngine;

using System;

using System.Collections;

 

public class NullableTest : MonoBehaviour {

   

    // Use this for initialization

    void Start () {

        this.CheckAndPrintInt(999999999);

        this.CheckAndPrintInt("九九九九九九九九九");

    }

   

    // Update is called once per frame

    void Update () {

       

    }

 

    void CheckAndPrintInt(object obj)

    {

        int? testInt = obj as int?;

        Debug.Log(testInt.HasValue ? testInt.Value.ToString() : "輸出的參數無法轉化為int");

    }

}

運行這個腳本之后,可以在Unity3D的調試窗口看到如下的輸出:

999999999

UnityEngine.Debug:Log(Object)

輸出的參數無法轉化為int

UnityEngine.Debug:Log(Object)

這樣,我們就通過“as”操作符,優雅的實現了將引用轉換為值的操作。

0x05 可空值類型的裝箱和拆箱

正如前面我們所說的那樣,可空值類型Nullable<T>是一個結構,一個值類型。因此如果代碼中涉及到將可空值類型轉換為引用類型的操作(例如轉化為object),裝箱便是不可避免的。

但是有一個問題,那就是普通的值類型是不能為空的,裝箱之后的值自然也不是空,但是可空值類型是可以表示空值的,那么裝箱之后應該如何正確的表示呢?正是由於可空值類型的特殊性,Mono運行時在涉及到可空值類型的裝箱和拆箱操作時,會有一些特殊的行為:如果Nullable<T>的實例沒有值時,那么它會被裝箱為空引用;相反,如果Nullable<T>的實例如果有值時,會被裝箱成T的一個已經裝箱的值。

如果要將已經裝箱的值進行拆箱操作,那么該值可以被拆箱成為普通類型或者是拆箱成為對應的可空值類型,換句話說,要么拆箱為T,要么拆箱成Nullable<T>。不過各位讀者應該注意的一點是,在對一個空引用進行拆箱操作時,如果要將它拆箱成普通的值類型T,則運行時會拋出一個NullReferenceException異常,這是因為普通的值類型是沒有空值的概念的;而如果要拆箱成為一個恰當的可空值類型,最后的結果便是拆箱成一個沒有值的可空值類型的實例。

下面我們通過一段代碼來演示一下剛剛所說的可空值類型的裝箱以及拆箱操作。

using UnityEngine;

using System;

using System.Collections;

 

public class NullableTest : MonoBehaviour {

   

    // Use this for initialization

    void Start () {

        //從正常的不可空的值類型int隱式轉換為Nullable<Int32>

        Int32? testInt = 999;

        //從null隱式轉換為Nullable<Int32>

        Int32? testNull = new Nullable<int>();

       

        object boxedInt = testInt;

        Debug.Log("不為空的可空值類型實例的裝箱:" + boxedInt.GetType());

       

        Int32 normalInt = (int) boxedInt;

        Debug.Log("拆箱為普通的值類型Int32:" + normalInt);

       

        testInt = (Nullable<int>) boxedInt;

        Debug.Log("拆箱為可空值類型:" + testInt);

       

        object boxedNull = testNull;

        Debug.Log("為空的可空值類型實例的裝箱:" + (boxedNull == null));

       

        testNull = (Nullable<int>) boxedNull;

        Debug.Log("拆箱為可空值類型:" + testNull.HasValue);

    }

   

    // Update is called once per frame

    void Update () {

       

    }

}

   在上面這段代碼中,我演示了如何將一個不為空的可空值類型實例裝箱后的值分別拆箱為普通的值類型(如本例中的int)以及可空值類型(如本例中的Nullable<int>)。之后,我又將一個沒有值的可空值類型實例testNull裝箱為一個空引用,之后又成功的拆箱為另一個沒有值的可空值類型實例。如果此時我們直接將它拆箱為一個普通的值類型,編譯器會拋出一個NullReferenceException異常,如果有興趣,各位讀者可以自己動手嘗試一下。


免責聲明!

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



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