C# 关于NULL 可空值类型 ? 和空接操作符??
作者 陈嘉栋(慕容小匹夫)
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异常,如果有兴趣,各位读者可以自己动手尝试一下。
C# 关于NULL 可空值类型 ? 和空接操作符??的更多相关文章
- 匹夫细说C#:可以为null的值类型,详解可空值类型
首先祝大家中秋佳节快乐~ 0x00 前言 众所周知的一点是C#语言是一种强调类型的语言,而C#作为Unity3D中的游戏脚本主流语言,在我们的开发工作中能够驾驭好它的这个特点便十分重要.事实上,怎么强 ...
- 【C#进阶系列】19 可空值类型
可空值类型,正如字面意义上的,是可以为NULL的值类型. 这个东西存在的意义可以解决比如数据库的的Int可以为NUll的情况,使得处理数据库数据更简单. 实际上可空值类型就是Nullable<T ...
- C#复习笔记(3)--C#2:解决C#1的问题(可空值类型)
可空值类型 C#2推出可空类型来表示可以为null的值类型.这是一个呼声很高的需求,因为在常用的数据库中都是允许某些值类型可为空的.那么为什么值类型就不能为空呢?内存中用一个全0的值来表示null,但 ...
- int? 竟然真的可以是 null!.NET/C# 确定可空值类型 Nullable 实例的真实类型
使用 Nullable<T> 我们可以为原本不可能为 null 的值类型像引用类型那样提供一个 null 值.不过注意:Nullable<T> 本身也是个 struct,是个值 ...
- Util应用程序框架公共操作类(十):可空值类型扩展
当你使用可空的值类型时,你会发现取值很不方便,比如Guid? obj,你要从obj中获取值,可以使用Value属性obj. Value,但obj可能为null,这时候就会抛出一个异常. 可空值类型提供 ...
- C#中的可空值类型
C# 不允许把 null 赋给一个值类型的数据.在 C# 中,以下语句是非法的: int a = null; // 非法 但是,利用 C# 定义的一个修饰符,可将一个变量声明为一个可空(null ...
- CLR via C#(14)-可空值类型,关于?和??的故事
我们都知道,值类型是不能为Null的,但是在实际应用中有些情形却需要将值类型置为null.因此,CLR中引用了可空值类型的用法.今天的文章中见到最多的符号估计就是?了吧. ?——初识可空值类型 1. ...
- [CLR via C#]19. 可空值类型
我们知道,一个值类型的变量永远不可能为null.它总是包含值类型本身.遗憾的是,这在某些情况下会成为问题.例如,设计一个数据库时,可将一个列定义成为一个32位的整数,并映射到FCL的Int32数据类型 ...
- .NET 可空值类型
Microsoft在CLR中引入了可空值类型(nullable value type)的概念. FCL中定义System.Nullable<T>类如下: [Serializable,Str ...
随机推荐
- android十六进制颜色代码转换为int类型数值
android开发中将十六进制颜色代码转换为int类型数值方法:Color.parseColor("#00CCFF")返回int数值;
- ORACLE EXP命令
本文对Oracle数据的导入导出 imp ,exp 两个命令进行了介绍, 并对其对应的參数进行了说明,然后通过一些演示样例进行演练,加深理解.文章最后对运用这两个命令可能出现的问题(如权限不够,不同o ...
- 刚才建立一个 swift 中文讨论社区,欢迎大家參与讨论
http://www.chinaswift.me 主要目的是收集 swift学习资源
- 一步一步写算法(之 A*算法)
[ 声明:版权全部,欢迎转载,请勿用于商业用途. 联系信箱:feixiaoxing @163.com] 在前面的博客其中,事实上我们已经讨论过寻路的算法.只是,当时的演示样例图中,可选的路径是唯一的 ...
- java web项目中的web.xml标签之context-param
WEB项目初始化过程: 在启动Web项目时,容器(比如Tomcat)会读web.xml配置文件中的两个节点<listener>和<contex-param>. 接着容器会创建一 ...
- iOS UIKit:viewController之层次结构(1)
ViewController是iOS应用程序中重要的部分,是应用程序数据和视图之间的重要桥梁.且应用程序至少有一个view controller.每个view controller对象都负责和管理一个 ...
- mac skim 修改背景色
defaults write -app skim SKPageBackgroundColor -array 0.78 0.93 0.80 1
- C++ 进阶必备
C++ 进阶要点(原理+熟练使用) 持续更新中 虚函数 虚继承 多继承 构造函数,拷贝构造函数,赋值构造函数,友元类,浅拷贝,深拷贝,运算符重载 class 类的基本使用,iostream获取屏幕输入 ...
- HBuilder+移动APP开发实例
mui: 官网:http://dcloudio.github.io/mui/ 说明:一般要把官网内容通读一遍,这是开发的基础 开始 1.新建项目 在首页点击新建移动App,如下: 或者在项目管理器内右 ...
- List<T>取交集、差集、并集
1. 取交集 (A和B都有) List A : { 1 , 2 , 3 , 5 , 9 }List B : { 4 , 3 , 9 }var intersectedList = list1.Inte ...