C# 可空上下文、可空引用类型(?)与可空容忍(!)
开始之前,先想一下,作为C#开发,你在开发过程中遇到的最多的异常是哪个?
不出意外,估计都是空指针引用,ArgumentNullException!
那么有没有办法尽量在开发过程检查出来,而不是等他在运行时报错?为此,微软老大哥一直在努力中。
可空上下文
从C#8.0开始,我们可以通过启用可空上下文,让VS在开发过程中可以检查我们出现的空指针引用异常。
启用可空上下文的方式有两种:
1、修改.csproj文件,添加<Nullable>enable</Nullable>节点,如:
<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net5.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup>
Nullable节点的值enable表示启用,disable表示停用。
此外需要注意,这种启用方式是全局性的,修改默认行为,默认是disable,但是从.net6开始,项目默认是启用可空上下文的,项目文件.csproj中会默认包含Nullable节点。
2、使用预编译指令#nullable enable来启用可空上下文,如:
//enable表示启用 #nullable enable //disable表示停用 #nullable disable
使用预编译指令表示局部性启用,和修改.csproj文件的方式配合使用。而#nullable enable和#nullable disable配合使用可以实现块级的局部性启用。
接下来看看可空上下文的用法,比如下面的代码:
object obj = null; obj.ToString();//throw System.ArgumentNullException
明显,上面的代码在运行时会抛出ArgumentNullException,但是在之前,我们在开发过程中得不到任何提示,生成项目也没有任何提示,但是启用可空上线文后,我们可以得到warning:
或者生成项目,错误窗口也会得到警告:
tip:作为一个开发者,不仅要处理项目中的任何一个异常,也应该正视项目抛出的任何一个警告!
可空引用类型(?)
可空值类型大家应该很熟悉了,它其实是Nullable<T>的对象,它可以认为是T值类型加上null的组合类型,而它的声明可以简写:
//T是值类型,T?本质上是Nullable<T>的简写,可以认为是T值类型加上null的组合类型 T?
从C#8.0开始,引入了可空引用类型,它同样可以按照上面可空值类型的方式进行简写,但是此时它就不是Nullable<T>的对象的对象了:
//T是引用类型,T?和Nullable<T>没有任何关系,T?只在编译时起作用,在运行时和T的作用是相同 T?
T?和Nullable<T>没有任何关系,T?只在编译时起作用,在运行时和T的作用是相同,引入可空引用类型,是为了在可空上下文中更好的检查空指针引用。
在可空上下文中,我们应该遵循以下原则来开发,假如T是引用类型,那么:
- 如果变量variable1在声明时使用的类型就是T,那么表示这个variable1是不能为null的引用类型变量,你可以直接使用它(比如调用方法属性等),而不需要做判断它是否为null,但是当你将一个null值或者可为null的值(如T?)赋值给这个variable1时,将会抛出警告!
class Program { #nullable enable static void Main(string[] args) { //声明为不可为null的引用类型 object obj = new object(); //当你将null值赋值时,将会得到警告 obj = null; //直接使用而不需要做判断它是否为null,但是当你将可空引用进行赋值时,将会得到警告 string str = obj.ToString(); } void Method(object obj)//方法中obj声明为不可为null的引用类型 { //你可以直接调用它的方法而不需要进行if判断是否为null int hashcode = obj.GetHashCode(); //直接使用而不需要做判断它是否为null,但是当你将可空引用进行赋值时,将会得到警告 string str = obj.ToString(); } }
- 如果变量variable1在声明时使用的类型就是T?,那么表示这个variable1是可为null的引用类型变量,也就是话,当你直接使用它去调用方法属性时,你需要提前判断它是否为null,或者使用null 条件运算符调用,否则将会得到警告
class Program { #nullable enable static void Main(string[] args) { //声明为可为null的引用类型 object? obj = null; //当你直接调用而没有做判断是否为null时,将会得到警告 string? str = obj.ToString(); //可以使用null条件运算符调用,没有警告 string? str1 = obj?.ToString(); } void Method(object? obj)//方法中obj声明为可为null的引用类型 { //当你直接调用而没有做判断是否为null时,将会得到警告 string? str = obj.ToString(); } }
- 如果一个变量variable1在声明时使用的类型就是T?,但是你在使用时断定它不会为null,或者你正在将一个T?的变量赋值给T的变量,如果按照上面第1、2点,你需要总是用if判断,不然又会多出一些警告,很是麻烦,这个时候就可以使用可空容忍(!)来避开它,这个下文再说
可空容忍(!)
其实,如果理解了可空上下文、可空引用类型,可空容忍就好理解了,他其实是一个补充,就是在代码中,如果我们断定某个变量在使用时一定不为null,但是编译器会在可空上下文中抛出警告,这是一个不太正常的行为,可空容忍可以帮助我们消除这种警告,可空容忍可以将不可为空的引用类型转换成可为空的引用类型,格式:
//T是引用类型,后台跟一个叹号(!),就是告诉编译器这个变量一定不为空,这样可以避免不必要的警告 T!
比如:
class Program { #nullable enable static void Main(string[] args) { //使用null会抛出警告,但是使用可空容忍告诉编译器消除警告 object obj = null!; //传入的变量一定不为null Method(new object()); } static void Method(object? obj)//方法中obj声明为可为null的引用类型 { //这里obj1一定不为null,但是按照可空规则,他将会抛出警告,但是可空容忍可以告诉编译器它一定不为null,从而消除错误的警告 string? str = obj!.ToString(); //可空容忍其实可以认为是将可为空的引用类型转换为不可为空的引用类型 string str1 = str!; } }
总结
总之,我们只需要记住,可空引用类型是在编译时起作用,在运行时和普通的引用类型没有任何区别,它主要是在编译时结合可空上下文,帮助我们分析代码中可能出现空指针引用异常的地方,这是一个非常好的语法糖,我们只需要遵守上面三种规则,就可以很大程度减少空指针异常的几率,其实,如果仔细看的话,.net基础库已经遵守了这个规则,比如object类的ToString方法和Equals方法等。
参考文档:
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-forgiving