您的位置:首页 > 编程语言 > C#

C# 关于NULL 可空值类型 ? 和空接操作符??

2016-08-30 19:36 155 查看
作者 陈嘉栋(慕容小匹夫)

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异常,如果有兴趣,各位读者可以自己动手尝试一下。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: