6.9 可空特性
在很多情况下,明确告诉编译器你需要处理空值,并需要为此添加一些防护措施,比笼统地关闭空值功能或者关闭空值警告更有意义。若要实现这一点,可以将一些元数据(metadata)作为特性(attribute)包含在代码中。第18章将更加深入地介绍特性这一概念。在System.Diagnostics.CodeAnalysis命名空间中定义了7种不同的可空特性,有些属于前置条件,有些属于后置条件。(见表6.1)
表6.1 可空特性
这些可空特性对编程很有帮助,因为有时将数据定义为可空或者不可空,并不足以提高程序的健壮性。为了弥补这种不足,你可以使用可空特性对方法的输入数据(使用前置条件可空特性)和输出数据(使用后置条件可空特性)进行描述。前置条件可空特性用来描述调用者输入的数据是否可以为空值,而后置条件可空特性用来描述调用者将接收到的数据会不会为空值。代码清单6.36中的两个名为“TryGet……”的函数展示了一个可空特性的应用示例。
代码清单6.36 NotNullWhen和NotNullIfNotNull特性的应用示例
观察上面代码你会注意到,即便digitText变量被定义为可空,后面在调用其ToLower方法时也没有使用空合并操作符。如果编译这段代码,会发现此处甚至不会产生编译器警告。这是因为在TryGetDigitAsText()方法的定义中,输出变量text被标记了NotNullWhen(true)特性。它告诉编译器,如果该方法返回true(即NotNullWhen特性中指定的值),则digit text不会为null。NotNullWhen是一个后置条件特性,因此它的条件是否成立会在方法结束后再被评估。有了这个认识,再次观察代码,可以注意到ToLower方法的调用发生在if判断的内部,因此只有当TryGetDigitAsText()返回true,该调用才会发生。而根据NotNullWhen特性的描述,此时digitText变量一定不会是null,所以这里既不需要使用空合并操作符,也不会产生编译器警告。
上面代码中的第二个方法TryGetDigitsAsText()[1]也采用了类似的可空特性描述。它用前置条件特性NotNullIfNotNull声明了:如果输入参数text不为null,则函数返回值也不为null。
高级主题:用可空特性修饰泛型类型
在泛型编程中,我们经常希望将泛型类型声明为可空。但是,由于可空值类型(派生于Nullable<T>)与可空引用类型本质上是完全不同的数据类型,因此如果要将泛型类型声明为可空,则必须将该泛型类型约束为值类型或者引用类型,否则,将会产生下面的编译错误:
但是,一个方法的逻辑有时候对于值类型和引用类型完全相同,如果因为上述原因而被迫写两份没有差别的代码,则会令人非常沮丧。更糟的是,如果采用类型约束,则由于类型约束无法产生不同的方法签名,导致无法使用重载,因此不得不将两份相同的代码写在两个不同名的方法中。在这种情况下,更好的解决办法是使用可空特性,如代码清单6.37所示。
代码清单6.37 为可能的null返回值使用MaybeNull特性
上面代码中的GetObject方法从sequence集合中寻找能够使match预测函数返回true的元素。如果这样的元素存在,则返回该元素;否则,返回null值。遗憾的是,编译器不允许在没有类型约束的情况下采用T?这样的写法[2],而如果忽略可空修饰符,则会产生编译器警告。为了解决这个问题,可以使用后置条件可空特性MaybeNull来代替可空修饰符。
[1] 注意该方法的名称与第一个方法不同。其中“Digits”一词多了字母“s”。——译者注
[2] 即为没有类型约束的泛型类型添加可空修饰符。——译者注