为什么值类型的变量不能是 null ?
对于一个引用类型的变量来说,其值是一个引用。而值类型变量的值是其值本身。
可以认为,一个非空引用值提供了访问一个对象的途径。然而,null 相当于一个特殊的值,它意味着“我不引用任何对象”。
内存中用全零来表示 null(这也解释了为什么所有引用类型的默认值是 null —— 清除一整块内存的速度是最快的,所以对象选择用这种方式来初始化),但它本质上采用的是和其他引用一样的方式来存储的。
为什么 null 不是一个有效的值类型的值? 假定有一个 byte 类型。byte 变量的值是用单独一个字节来存储的--出于对齐的目的,可能会进行一些填充。但是,值本身在概念上只由一个字节构成。我们可以将值 0~255 存储到那个变量中,如果试图将超出这个范围的值存储到其中,那么读取到的就是“垃圾”数据。所以,256 个“普通”值加 1 个 null 值,我们总共要处理 257 个值。没有办法用一个字节存储那么多的值。为此,设计者可能决定,为每个值类型都设置一个额外的标志位(fag bit),这个标志位用于判断一个值是 null 还是一个“真正”的值。但这样一来,内存的消耗将急剧增加,更不用说每次想要使用值时,都得对这个标志位进行检查。所以,简单地说,对于值类型,我们希望它拥有一套和真正的值一样的完整的位模式(bit pattern),而对于引用类型,则希望丢失一个潜在的值,用它换取能使用一个 null 值的便利。
引入的可空类型 System.Nullable<T> 和 System.Nullable
Nullable<T> 是一个泛型类型。类型参数 T 有一个值类型约束。
// 源码
public struct Nullable<T> where T : struct
对于一个 Nullable<int> x,理解它的这些属性与方法:
- x.HasValue
- x.Value
- x.GetValueOrDefault()
- x.GetValueOrDefault(10)
- x.ToString()
- x.GetHashCode()
Nullable<T> 装箱和拆箱
Nullable<T> 是一个结构 (struct) —— 一个值类型。这意味着如果把它转换成引用类型(object 是最明显的一个例子),就要对它进行装箱。
- 一个有值的可空类型的实例 如 Nullable<int> nullable = 5; 装箱后object boxed = nullable; 变量 boxed 既可以拆箱成 int,又可以拆箱成 Nullable<int> 。
- 一个没有值的可空类型的实例 如 nullable = new Nullable<int> (); boxed = nullable; 装箱成空引用,即 boxed 为 null。变量 boxed 可以拆箱成 Nullable<int> ,但不能拆箱成 int,程序会抛出一个 NullReferenceException。
Nullable<T> 实例的相等性
Nullable<T> 覆盖了 object.Equals(object),但没有引入任何相等性操作符,也没有提供 Equals(Nullable<T>) 方法。以下是 调用 first.Equals(second) 的具体规则:
public override bool Equals(object other)
{
if (!hasValue) // 没有值
{
return other == null; // 如果first没有值,second为 null,它们就是相等的; second不为 null,它们就是不相等的;
}
if (other == null) // 有值
{
return false; // first 有值,second为 null,它们就是不相等的;
}
return value.Equals(other); // 否则,如果 first 的值等于 second,它们就是相等的。
}
即 Nullable<T> 是一个泛型结构体,专门设计用于为 值类型 提供 null 值的能力。(可空类型也是值类型,它是包含 null 值的值类型。)
来自非泛型 Nullable 类的支持
Nullable 是一个静态类。包含的三个静态方法:
1. public static int Compare<T>(T? n1, T? n2) where T : struct
2. public static bool Equals<T>(T? n1, T? n2) where T : struct
上述两个方法返回的值都遵从.NET的约定:空值与空值比较才认为相等,空值小于其他所有值。
3. public static Type GetUnderlyingType(Type nullableType) (这不是泛型方法)
C#2 为可空类型提供的语法糖
Nullable<T> 可以用 T? 来表示。例:
int? nullable = null; // int? 就是可空的 int 类型。
C#2才引入对空值的定义:可空类型的空值是指在 “HasValue“ 返回 “false” 时的值。或者是 “实例没有值” 时的值。
这个定义也适用于引用类型:引用类型的空值是指空引用。
可空转换和操作符
假如一个 非可空的值类型 支持一个操作符或者一种转换,而且那个操作符或者转换只涉及 其他非可空的值类型 时,那么 可空的值类型 也支持相同的操作符或转换,并且通常是将非可空的值类型转换成它们的可空等价物。
例如,我们知道 int 到 long 存在着一个隐式转换,而那意味着 int? 到 long? 也存在一个隐式转换。
涉及可空类型的转换
先来看看我们已经知道的三种转换:
int? x = null; // null 到 T? 的隐式转换
int y1 = 2;
int? y2 = y1; // T 到 T? 的隐式转换
int? z1 = 5;
int z2 = (int)z1; // T? 到 T 的显式转换
在 (int)z1 显式转换中,直接等于获取 可为空对象的 Value 属性。 它会先判断若 HasValue 为 false,则会抛出 InvalidOperationException,true 则返回 值。
现在来看看类型所支持的预定义转换和用户自定义转换。例如,int 到 long 存在一个预定义转假如允许从非可空值类型(s)转换成另一个非可空值类型(),那么同时允许进行以下转换:
// 假如允许从非可空值类型(S)转换成另一个非可空值类型(T)
int? x1 = 1;
int? x2 = x1; // S? 到 T? 隐式 (同一类型)
long? x3 = x1; // S? 到 T? 隐式 (不同类型,低字节数转向高字节数)
byte? x4 = (byte?)x1; // S? 到 T? 显式 (不同类型,高字节数转向低字节数)
// S? 到 T?(可能是显式或隐式的,具体取决于原始转换);
还这意味着可以隐式地从 int? 转换为 long?,隐式地从 int 转换为long?,以及显式地从 int? 转换为long。转换过程是自然而体贴的,s? 的空值会转换为 T? 的空值。如果转换的是非空的值,就使用原始转换。和往常一样,如果 s? 是空值,那么从 s? 到的显式转换会抛出一个 InvalidOperationException。对于用户自定义的转换,这些涉及可空类型的额外转换称为提升转换 (lifed conversion)。
int 和 null 能做比较吗?
int i = 5;
if (i == null)
{
Console.WriteLine ("Never going to happen");
}
if (i == null) 这句代码可以通过编译,对于左侧的 i 和 右侧的 null,编译器知道两者都存在到 int? 的一个隐式转换。由于两个 int? 值进行比较是完全有效的,所以代码不会产生错误——只会产生警告:由于 "int" 类型的值永不等于 "int?" 类型的 "null",该表达式的结果始终为"false" 。
所以对于这段代码:
DateTime birth;
DateTime? death;
// Age 属性
public TimeSpan Age
{
get
{
if (death == null) // 这句实际调用了 death.HasValue 属性
{
return DateTime.Now - birth;
}
else
{
return death.Value - birth; // 拆包以进行计算
}
}
}
为什么要使用 death.Value 对值进行拆包处理呢? 为什么不能直接 return death - birth 呢? 在 death 为 null 的情况下,我们希望表达式表达出一个什么样的意思?
death - birth 这个表达式是有用的,但是结果就会是一个 TimeSpan?,而不是 TimeSpan。用 TimeSpan? 只是将问题转嫁到了调用者身上,他要再次判断。
对可空类型使用 as 操作符
在 C# 2 之前,as 操作符只能用于引用类型。而在 C#2 中,它也可以用于可空类型。
static void PrintValueAsInt32(object o)
{
int? nullable = o as int?;
Console.WriteLine(nullable.HasValue ? nullable.Value.ToString() : "null");
}
PrintValueAsInt32(5); // 5
PrintValueAsInt32("some string"); // null
这样我们仅需一步就可以安全地将任意引用转换为值。而在 C#1 中,你只能先使用 is 操作符,然后再强制转换,这会使 CLR 执行两次相同的类型检查,显然不够优雅。
但在性能上,as 并不比 is 快。
当引入空合并操作符后 ??
就可以精简表示式为:
return (death ?? DateTime.Now) - birth;
?? 操作符的结合性是"右结合"。这意味着表达式 first ?? second ?? third 实际相当于 first ?? (second ?? third) 。如果还有更多操作数,可以此类推。
小结
C#2 的可空类型的工作方式,是增加额外的布尔标志。即基本思路是使用一个普通的值类型的值,同时用另一个值(一个布尔标志)来表示值是否是“真正”存在。