匹夫细说C#:可以为null的值类型,详解可空值类型

时间:2022-08-26 23:08:43

首先祝大家中秋佳节快乐~

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() : ;

        }

        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 ;

            }

            if (n2.HasValue) return -;

                return ;

            }

        [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()[];

                }

            }

            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 = ;

        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 = ;

        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 = ;

        //从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 = ;

        Int32? testNull = null;

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

        testInt ++;

        testNull = -testNull;

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

        testInt = testInt + ;

        testNull = testNull * ;

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

        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 ?? ;

              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();

        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 = ;

        //从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异常,如果有兴趣,各位读者可以自己动手尝试一下。