引用类型可以使用空引用表示一个不存在的值,然而值类型不能直接表示为null:
若要在值类型中表示null,则必须使用特殊的结构即可空值类型。可空值类型是由值类型后加一个“?”表示的:
T? 会转换为 System.Nullable<T> ,它是一个轻量级的不可变的结构体。它只有两个字段,分别代表 Value 和 HasValue 。 System.Nullable<T> 的本质是很简单的:
以下代码:
将转换为:
当 HasValue 为false时,试图获得 Value 会抛出 InvalidOperationException 异常。当 HasValue 为true时, GetValueOrDefault() 会返回 Value ,否则返回 new T() 或者一个特定的自定义默认值。
T? 的默认值为 null 。
从 T 到 T? 的转换是隐式的,反之则是显式的。例如:
显式强制转换与直接调用可空对象的 Value 属性实质上是等价的。因此,当 HasValue 为false的时候将抛出 InvalidOperationException 。
当 T? 类型的对象装箱后,堆中的装箱值包含的是 T ,而非 T? 。这种优化方式是可行的,因为装箱值已经是一个可以赋值为null的引用类型了。
C#允许通过 as 运算符对一个可空值类型进行拆箱。如果强制转换出错,那么结果为 null :
Nullable<T> 结构并没有定义诸如 < 、 > 、 == 之类的运算符。尽管如此,以下代码仍然能够正常编译和执行:
这是因为编译器会从实际值类型借用或者“提升”小于运算符。在语义上,它会将前面的比较表达式转换为如下语句:
换句话说,如果 x 和 y 都有值,那么它会通过 int 的小于运算符做比较。否则它会返回 false 。
运算符提升意味着可以隐式使用 T 的运算符来处理 T? 。可以专门针对 T? 进行运算符重载来实现特殊的空值行为。但是在大多数情况下,最好通过编译器来自动地应用系统的空值逻辑。以下是一些示例:
编译器会根据运算符的分类来执行空值逻辑。下面将介绍这些不同的规则。
提升后的相等运算符可以像引用类型那样处理空值,这意味着两个 null 值是相等的。
而且:
· 如果只有一个操作数为null,那么两个操作数不相等。
· 如果两个操作数都不能为null,则比较它们的 Value 。
对于关系运算符而言比较null操作数是没有意义的。因此比较空值和另外一个空值或非空值的结果都是 false 。
当任意一个操作数为 null 时,此类运算符都会返回 null 。SQL用户是非常熟悉这种模式的:
唯一的例外是计算 bool? 的 & 和 | 运算符,我们稍后会进行详细讨论。
混合使用可空或不可空值类型是可行的,这是因为 T 与 T? 之间存在着隐式转换机制:
如果操作数的类型为 bool? ,那么 & 和 | 运算符会将 null 作为一个未知值(unknown value)看待。所以 null | true 应当返回 true ,因为:
· 如果未知值是假的,那么结果为真。
· 如果未知值是真的,那么结果为真。
类似地, null & false 的结果为 false 。这个行为和SQL非常相似,以下例子说明了一些其他组合用法:
可空值类型与 ?? 运算符相辅相成(请参见2.10.1节),如以下示例所示:
在可空值类型上使用 ?? 运算符相当于调用 GetValueOrDefault 方法并提供一个显式的默认值,但变量如果不是 null 的话则不会使用默认值。
可空值类型同样适用于 null 条件运算符(请参见2.10.3节)。在下面的例子中,length的值为 null :
结合使用null合并运算符和null条件运算符可最终得到0而不是null:
可空值类型常用来表示未知的值,尤其是在数据库编程中最为常见。数据编程通常需要将类映射到具有可空列的数据表中。如果这些列是字符串类型(例如,Customer表的EmailAddress列),这样就没有任何问题,因为字符串是一种CLR的引用类型,所以可以为null。然而有些SQL列的类型是值类型,因此使用可空值类型可以将这些SQL的列映射到CLR中。例如:
可空值类型还可以表示支持字段,即所谓的环境属性(ambient property)。如果环境属性的值为null,则返回父一级的值。例如:
在可空值类型成为C#语言的一部分之前(例如C# 2.0以前的版本),也有许多处理可空值类型的方式。出于历史原因,这些方式现在仍然存在于.NET库中,其中一种方式是将一个特定的非空值指定为“空值”。字符串和数组类中就使用了这种方式。例如, String.IndexOf 在找不到字符时会返回一个特殊的“魔法值” -1 。
然而, Array.IndexOf 只有在索引是基于 0 的时候才会返回 -1 。实际的规则是 IndexOf 返回比数据下限小1的值。在下一个例子中, IndexOf 在没有找到某个元素的时候返回 0 。
指定“魔法值”会造成各种问题,以下列举了一些原因:
· 每一个值类型有不同的空值表示方式。而与之相反,使用可空值类型可以用一种通用模式处理任意的值类型。
· 可能无法找到一个合理的值。例如,在上述的例子中,我们无法总是使用 -1 。更早的例子中表示一个未知账号的余额的方式也有相同的问题。
· 如果忘记对魔法值进行测试可能导致长期忽略不正确的数据,直至在后续运行中出现一个出乎意料的结果。而如果忘记测试 HasValue 为null的情况,则会马上抛出 InvalidOperationException 。
· 没有在类型层面上处理null值的能力。类型可以传达程序的意图,并允许编译器检查其正确性,从而和编译器的规则保持一致。