首页 > 编程笔记 > C#笔记 阅读:1

C#可空值类型用法详解(附带实例)

C# 中,引用类型可以使用空引用表示一个不存在的值,然而值类型不能直接表示为 null:
string s = null;   // OK, Reference Type
int i = null;      // Compile Error, Value Type cannot be null

若要在值类型中表示 null,则必须使用特殊的结构即可空值类型。可空值类型是由值类型后加一个“?”表示的:
int? i = null;     // OK, Nullable Type
Console.WriteLine(i == null);   // True

T? 会转换为 System.Nullable<T>,它是一个轻量级的不可变的结构体。它只有两个字段,分别代表 Value 和 HasValue。System.Nullable<T> 的本质是很简单的:
public struct Nullable<T> where T : struct
{
    public T Value { get; }
    public bool HasValue { get; }
    public T GetValueOrDefault();
    public T GetValueOrDefault(T defaultValue);
}
以下代码:
int? i = null;
Console.WriteLine(i == null);   // True
将转换为:
Nullable<int> i = new Nullable<int>();
Console.WriteLine(!i.HasValue);   // True
当 HasValue 为 false 时,试图获得 Value 会抛出 InvalidOperationException 异常。当 HasValue 为 true 时,GetValueOrDefault() 会返回 Value,否则返回 new T() 或者一个特定的自定义默认值。

T? 的默认值为 null。从 T 到 T? 的转换是隐式的,反之则是显式的。例如:
int? x = 5;   // implicit
int y = (int)x;   // explicit
显式强制转换与直接调用可空对象的 Value 属性实质上是等价的。因此,当 HasValue 为 false 的时候将抛出 InvalidOperationException。

当 T? 类型的对象装箱后,堆中的装箱值包含的是 T,而非 T?。这种优化方式是可行的,因为装箱值已经是一个可以赋值为 null 的引用类型了。

C# 允许通过 as 运算符对一个可空值类型进行拆箱。如果强制转换出错,那么结果为 null:
object o = "string";
int? x = o as int?;
Console.WriteLine(x.HasValue);   // False

运算符优先级提升

Nullable<T> 结构并没有定义诸如 <、>、== 之类的运算符。尽管如此,以下代码仍然能够正常编译和执行:
int? x = 5;
int? y = 10;
bool b = x < y;  // true
这是因为编译器会从实际值类型借用或者“提升”小于运算符。

在语义上,它会将前面的比较表达式转换为如下语句:
bool b = (x.HasValue && y.HasValue) ? (x.Value < y.Value) : false;
换句话说,如果 x 和 y 都有值,那么它会通过 int 的小于运算符做比较。否则它会返回false。

运算符提升意味着可以隐式使用 T 的运算符来处理T?。可以专门针对T?进行运算符重载来实现特殊的空值行为。但是在大多数情况下,最好通过编译器来自动地应用系统的空值逻辑。以下是一些示例:
int? x = 5;
int? y = null;

// Equality operator examples
Console.WriteLine(x == y);   // False
Console.WriteLine(x == null);   // False
Console.WriteLine(x == 5);   // True
Console.WriteLine(y == null);   // True
Console.WriteLine(y == 5);   // False
Console.WriteLine(y != 5);   // True

// Relational operator examples
Console.WriteLine(x < 6);   // True
Console.WriteLine(y < 6);   // False
Console.WriteLine(y > 6);   // False

// All other operator examples
Console.WriteLine(x + 5);   // 10
Console.WriteLine(x + y);   // null (prints empty line)

编译器会根据运算符的分类来执行空值逻辑。下面将介绍这些不同的规则。

1) 相等运算符(==和!=)

提升后的相等运算符可以像引用类型那样处理空值,这意味着两个 null 值是相等的:
Console.WriteLine(null == null);   // True
Console.WriteLine((bool?)null == (bool?)null);   // True
而且:

2) 关系运算符(<、<=、>=、>)

对于关系运算符而言比较 null 操作数是没有意义的。因此比较空值和另外一个空值或非空值的结果都是 false。
bool b = x < y;   // Translation:
bool b = (x.HasValue && y.HasValue) ? (x.Value < y.Value) : false;
// b is false (assuming x is 5 and y is null)

3) 其他运算符

当任意一个操作数为 null 时,+、-、*、/、%、&、|、^、<<、>>、+、++、--、! 和 ~ 运算符都会返回 null。SQL 用户是非常熟悉这种模式的:
int? c = x + y;   // Translation:
int? c = (x.HasValue && y.HasValue) ? (int?)(x.Value + y.Value) : null;
// c is null (assuming x is 5 and y is null)
唯一的例外是计算 bool? 的 & 和 | 运算符。

4) 混合使用可空和非空类型的操作数

混合使用可空或不可空值类型是可行的,这是因为 T 与 T? 之间存在着隐式转换机制:
int? a = null;
int b = 2;
int? c = a + b;   // c is null - equivalent to a + (int?)b

在bool?上使用&和|运算符

如果操作数的类型为 bool?,那么 & 和 | 运算符会将 null 作为一个未知值(unknown value)看待。所以 null | true 应当返回 true,因为:
类似地,null & false 的结果为 false。这个行为和 SQL 非常相似,以下例子说明了一些其他组合用法:
bool? n = null;
bool? f = false;
bool? t = true;

Console.WriteLine(n | n);   // (null)
Console.WriteLine(n | f);   // (null)
Console.WriteLine(n | t);   // True
Console.WriteLine(n & n);   // (null)
Console.WriteLine(n & f);   // False
Console.WriteLine(n & t);   // (null)

可空值类型和null运算符

可空值类型与 ?? 运算符相辅相成,如以下示例所示:
int? x = null;
int y = x ?? 5;   // y is 5

int? a = null, b = 1, c = 2;
Console.WriteLine(a ?? b ?? c);   // 1 (first non-null value)
在可空值类型上使用 ?? 运算符相当于调用 GetValueOrDefault 方法并提供一个显式的默认值,但变量如果不是 null 的话则不会使用默认值。

可空值类型同样适用于 null 条件运算符。在下面的例子中,length 的值为 null:
System.Text.StringBuilder sb = null;
int? length = sb?.ToString().Length;

结合使用 null 合并运算符和 null 条件运算符可最终得到 0 而不是 null:
int length = sb?.ToString().Length ?? 0;   // Evaluates to 0 if sb is null

C#可空值类型的应用场景

可空值类型常用来表示未知的值,尤其是在数据库编程中最为常见。

数据库编程通常需要将类映射到具有可空列的数据表中。如果这些列是字符串类型(例如,Customer 表的 EmailAddress 列),这样就没有任何问题,因为字符串是一种 CLR 的引用类型,所以可以为 null。

然而有些 SQL 列的类型是值类型,因此使用可空值类型可以将这些 SQL 的列映射到 CLR 中。例如:
// Maps to a Customer table in a database
public class Customer
{
   ...
   public decimal? AccountBalance;
}

可空值类型还可以表示支持字段,即所谓的环境属性(ambient property)。如果环境属性的值为 null,则返回父一级的值。例如:
public class Row
{
    ...
    Grid parent;
    Color? color;

    public Color Color
    {
        get { return color ?? parent.Color; }
        set { color = value == parent.Color ? (Color?)null : value; }
    }
}

可空值类型的替代方案

在可空值类型成为 C# 语言的一部分之前,也有许多处理可空值类型的方式。出于历史原因,这些方式现在仍然存在于 .NET 库中,其中一种方式是将一个特定的非空值指定为“空值”。字符串和数组类中就使用了这种方式。

例如,String.IndexOf 在找不到字符时会返回一个特殊的“魔法值” -1:
int i = "pink".IndexOf('b');
Console.WriteLine(i); // -1

然而,Array.IndexOf 只有在索引是基于 0 的时候才会返回 -1。实际的规则是 IndexOf 返回比数据下限小 1 的值。在下一个例子中,IndexOf 在没有找到某个元素的时候返回 0:
// Create an array whose lower bound is 1 instead of 0:
Array a = Array.CreateInstance(typeof(string),
                               new int[] { 2 },
                               new int[] { 1 });

a.SetValue("a", 1);
a.SetValue("b", 2);

Console.WriteLine(Array.IndexOf(a, "c")); // 0
注意,指定“魔法值”会造成各种问题,以下列举了一些原因:

相关文章