![C# 8.0本质论](https://wfqqreader-1252317822.image.myqcloud.com/cover/306/43475306/b_43475306.jpg)
6.9 可空特性
在很多情况下,明确告诉编译器你需要处理空值,并需要为此添加一些防护措施,比笼统地关闭空值功能或者关闭空值警告更有意义。若要实现这一点,可以将一些元数据(metadata)作为特性(attribute)包含在代码中。第18章将更加深入地介绍特性这一概念。在System.Diagnostics.CodeAnalysis命名空间中定义了7种不同的可空特性,有些属于前置条件,有些属于后置条件。(见表6.1)
表6.1 可空特性
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/6b1.jpg?sign=1738819256-GCppAJYQ8gN94rf8EFHuwE5VYsmxFXBl-0-d8f799ca282624f6fbc0a0fee8c9e49e)
这些可空特性对编程很有帮助,因为有时将数据定义为可空或者不可空,并不足以提高程序的健壮性。为了弥补这种不足,你可以使用可空特性对方法的输入数据(使用前置条件可空特性)和输出数据(使用后置条件可空特性)进行描述。前置条件可空特性用来描述调用者输入的数据是否可以为空值,而后置条件可空特性用来描述调用者将接收到的数据会不会为空值。代码清单6.36中的两个名为“TryGet……”的函数展示了一个可空特性的应用示例。
代码清单6.36 NotNullWhen和NotNullIfNotNull特性的应用示例
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d6.36.jpg?sign=1738819256-o5mZYZeAUWoqwaOSZ1cTOLeNusS9cl4m-0-9699f5afdf0b7df9ac1304aa949070c6)
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d6.36x.jpg?sign=1738819256-pMPOx42OJXA5VTudpX82IGrUV0xR4DQf-0-e9df2ca3088da335379cbf39d6590c0a)
观察上面代码你会注意到,即便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>)与可空引用类型本质上是完全不同的数据类型,因此如果要将泛型类型声明为可空,则必须将该泛型类型约束为值类型或者引用类型,否则,将会产生下面的编译错误:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/238-2-i.jpg?sign=1738819256-pew5i5Gllp00icr9vlAd0jyhoZaMG6ui-0-09722710e1b1711db78125460f0dc127)
但是,一个方法的逻辑有时候对于值类型和引用类型完全相同,如果因为上述原因而被迫写两份没有差别的代码,则会令人非常沮丧。更糟的是,如果采用类型约束,则由于类型约束无法产生不同的方法签名,导致无法使用重载,因此不得不将两份相同的代码写在两个不同名的方法中。在这种情况下,更好的解决办法是使用可空特性,如代码清单6.37所示。
代码清单6.37 为可能的null返回值使用MaybeNull特性
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d6.37.jpg?sign=1738819256-quWiaqbjr2wr7Jcj9DTj41sSnNgb6csb-0-3fce382e9bc6be2b906814a45f8e8036)
上面代码中的GetObject方法从sequence集合中寻找能够使match预测函数返回true的元素。如果这样的元素存在,则返回该元素;否则,返回null值。遗憾的是,编译器不允许在没有类型约束的情况下采用T?这样的写法[2],而如果忽略可空修饰符,则会产生编译器警告。为了解决这个问题,可以使用后置条件可空特性MaybeNull来代替可空修饰符。
[1] 注意该方法的名称与第一个方法不同。其中“Digits”一词多了字母“s”。——译者注
[2] 即为没有类型约束的泛型类型添加可空修饰符。——译者注