可空类型 Nullable

为什么值类型的变量不能是 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 的可空类型的工作方式,是增加额外的布尔标志。即基本思路是使用一个普通的值类型的值,同时用另一个值(一个布尔标志)来表示值是否是“真正”存在。

  • 7
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值