第 19 章 可空值类型

第 19 章 可空值类型

本章内容

现在,要在代码中使用一个可空的 Int32,就可以像下面这样写:

1
2
3
4
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());

编译并运行上述代码,将获得以下输出:

1
2
x: HasValue=True, Value=5
y: HasValue=False, Value=0

19.1 C#对可空值类型的支持

C# 允许用问号表示法来声明并初始化 xy 变量:

1
2
Int32? x = 5;
Int32? y = null;

在 C#中, Int32? 等价于 Nullable<Int32>。但 C# 在此基础上更进一步,允许开发人员在可空实例上执行转换和转型。C# 还允许向可空实例应用操作符。以下代码对此进行了演示:

① 作者在这里区分了转换和转型。例如,从 Int32 的可空版本到非可空版本(或相反),称为“转换”。但是,涉及不同基元类型的转换,就称为“转型”或“强制类型转换”。 ———— 译注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.0)
Double? e = b; // Int32?转型为 Double? (e 是 null)
}

C# 还允许向可空实例应用操作符,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 */ }
}

注意,操作可空实例会生成大量代码。例如以下方法:

1
2
3
private static Int32? NullableCodeSize(Int32? a, Int32? b) {
return (a + b);
}

编译器生成的 IL 代码等价于以下 C# 代码:

1
2
3
4
5
6
7
8
private static Nullable<Int32> NullableCodeSize(Nullable<Int32> a, Nullable<Int32> b) {
Nullable<Int32> nullable1 = a;
Nullable<Int32> nullable2 = a;
if (!(nullable1.HasValue & nullable2.HasValue)) {
return new Nullable<Int32>();
}
return new Nullable><Int32> (nullable1.GetvalueOrDefault() + nullable2.GetValueOrDefault());
}

最后要说明的是,可定义自己的值类型来重载上述各种操作符符。使用自己的值类型的可空实例,编译器能正确识别它并调用你重载的操作符(方法)。

19.2 C#的空接合操作符

C# 提供了一个“空接合操作符”(null-coalescing operator),即??操作符,它要获取两个操作数。假如左边的操作数不为 null,就返回这个操作数的值。如果左边的操作数为 null,就返回右边的操作数的值。利用空接合操作符,可以方便地设置变量的默认值。

空接合操作符的一个好处在于,它既能用于引用类型,也能用于可空值类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
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";
}

?? 在复合情形中更好用。例如,下面这行代码:

String s = SomeMethod1() ?? SomeMethod2() ?? "Untitled";

19.3 CLR 对可空值类型的特殊支持

CLR 内建对可空值类型的支持。这个特殊的支持是针对装箱、拆箱、调用 GetType 和调用接口方法提供的,它使可空类型能无缝地集成到 CLR 中

19.3.1 可空值类型的装箱

1
2
3
4
5
6
7
8
// 对 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"

19.3.2 可空值类型的拆箱

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建已装箱的 Int32
Object o = 5;

//
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

19.3.3 通过可空值类型调用 GetType

Nullable<T> 对象上调用 GetType,CLR实际会“撒谎”说类型是 T,而不是 Nullable<T>。以下代码演示了这一行为:

1
2
3
Int32? x = 5;
// 下面这行会显示 "System.Int32",而非“System.Nullable<Int32>”
Console.WriteLine(x.GetType());

19.3.4 通过可空值类型调用接口方法

1
2
3
Int32? n = 5;
Int32 result = ((IComparable) n).CompareTo(5); // 能顺利编译和运行
Console.WriteLine(result); // 0