为你的项目启用可空引用类型
Intro
C# 从 8.0 开始引入了可空引用类型,我们可以为项目启用可空引用类型来借助编译器来帮助我们更好的处理代码中的空引用的处理,可以避免我们写很多不必要 null 检查,提高我们的效率
Why
为什么我们要启用可空引用类型呢,首先我们可以看一下 asp.net core 项目,asp.net core 的项目正在大量的使用可空引用类型,详情可以参考:
https://github.com/dotnet/aspnetcore/issues/5680
Updating ASP.NET Core to use C# 8’s nullable reference types would:
- Help ASP.NET Core libraries avoid null reference exceptions internally. It will help us find and prevent our bugs and increase our developer productivity
- Provide guidance to developers who are using ASP.NET Core about which APIs can accept and return a null reference and which APIs can’t. This would improve the developer experience of using ASP.NET Core
主要分为两方面,一方面是内部的代码,对于内部代码而言,使用可空引用类型我们可以借助编译器清晰地了解一个变量是否会为 null ,不会为 null 的变量就不再需要进行空检查了,另一方面是对于使用的代码,对于使用启用空引用类型的类库,编译器可以提供更好的空检查支持,开发者可以清晰地了解哪些 API 是允许为 null,哪些 API 是不允许为 null 的,对开发者更为友好
How
接着我们就来看一看如何为我们的项目启用可空引用类型吧,微软的文档上提供了比较详细的说明,详细可以参考文末的引用链接
启用可空引用类型只需要在项目文件中添加 <Nullable>enable</Nullable> 即可,LangVersion 需要设置为 8 及以上。
Nullable 上下文包含了两个上下文一个是 Nullable annotation context(支持 ? 表示可为空的引用类型),一个是 Nullable warning context(支持编译器针对可空引用类型的警告)
Nullable 上下文有 4 种配置,配置如下
enablewarningsannotationsdisable
| Setting | Warning Context Status | Annotation Context Status |
|---|---|---|
| enable | enabled | enabled |
| warning | enabled | disabled |
| annotations | disabled | enabled |
| disable | disabled | disabled |
推荐直接使用 enable 启用可空引用类型,只启用 annotation 上下文,编译器不会针对可空引用类型的检查做出警告,意义就不太大了,只启用 warning 上下文,可以使用在不想在自己应用中启用可空引用类型,可以尝试这个配置,不配置 nullable 或者配置 disable 则可以完全禁用可空引用类型
除了针对 project 的 global 的配置之外,我们还可以在项目源代码里通过 #nullable 来改变局部的可空上下文配置,通过 #nullable restore 恢复默认的可空引用上下文配置
#nullable enable: 设置 nullable annotation context 和 nullable warning context 为 enabled.#nullable disable: 设置 nullable annotation context 和 nullable warning context 为 disabled.#nullable restore: 恢复 nullable annotation context 和 nullable warning context 为项目默认的配置.#nullable disable warnings: 设置 nullable warning context 为 disabled.#nullable enable warnings: 设置 nullable warning context 为 enabled.#nullable restore warnings: 恢复 nullable warning context 为项目配置#nullable disable annotations: 设置 nullable annotation context 为 disabled.#nullable enable annotations: 设置 nullable annotation context 为 enabled.#nullable restore annotations: 恢复 annotation warning context 为项目配置
启用可空引用类型之后,引用类型就不允许被设置为 null,如果要设置为 null,可以在类型后加一个 ? 设置为可空的引用类型如 string? ,或者使用 ! 让编译器允许赋值,如:string a = null!;(这也是我们需要注意的一个地方,可空引用类型只是编译器的检查,并不能够严格的保证不会被赋值为 null,对于类库项目,如果public 的 API 期望的参数是不可空的引用类型,除了使用不可空引用类型外,还是需要保留 null 检查)
Sample
首先可以看一个接口:
public interface IPropertyConfiguration<out TEntity, TProperty>
{
IPropertyConfiguration<TEntity, TProperty> HasColumnTitle(string title);
IPropertyConfiguration<TEntity, TProperty> HasColumnFormatter(string? formatter);
IPropertyConfiguration<TEntity, TProperty> HasColumnInputFormatter(Func<string?, TProperty?>? formatterFunc);
}
来看实现:
internal sealed class PropertyConfiguration<TEntity, TProperty> : PropertyConfiguration, IPropertyConfiguration<TEntity, TProperty>
{
private readonly PropertyInfo _propertyInfo;
public PropertyConfiguration(PropertyInfo propertyInfo)
{
_propertyInfo = propertyInfo;
PropertyName = propertyInfo.Name;
ColumnTitle = propertyInfo.Name;
}
public IPropertyConfiguration<TEntity, TProperty> HasColumnTitle(string title)
{
ColumnTitle = title ?? throw new ArgumentNullException(nameof(title));
return this;
}
public IPropertyConfiguration<TEntity, TProperty> HasColumnFormatter(string? formatter)
{
ColumnFormatter = formatter;
return this;
}
public IPropertyConfiguration<TEntity, TProperty> HasInputFormatter(
Func<TEntity?, TProperty?, TProperty?>? formatterFunc)
{
InternalCache.InputFormatterFuncCache.AddOrUpdate(_propertyInfo, formatterFunc);
return this;
}
}
可以看到 HasColumnTitle 的参数中的 title 是不可空的引用类型,即使如此实现代码里还是做了 null 检查,而且可空引用类型在 throw new ArgumentNullException() 的时候也不会引发警告
警告示例:
如果赋值 null 给一个不可为空的引用类型时,编译器就会给出一个警告,示例如下:

在往一个不可空引用类型列表里中添加 null 时,编译器也会给出一个警告:

如果一个可空的的引用类型变量没有检查 null 的时候,也会有警告:

从上图中可以看出,使用 var 声明变量的时候,会是一个可空的引用类型
More
使用可空引用类型可以一定程度上帮助我们减少不必要的 null 检查,但是对于类库项目来说,该有的 null 检查还是要有的
对于应用来说,借助可空引用类型也可以比较清晰地了解,哪些地方需要检查 null,哪些地方不需要,可以提升代码质量
对于 null 包容运算符 ! ,可以将一个可能 null 的对象赋值给不可空的引用类型变量,尽量不用使用,用了这个就是自己在代码里埋雷,本来不会为 null 的变量、属性也会出现 null 的情况,如果还没有必要的 null 检查,完全是自己给自己挖坑。
但是在使用过程中,感觉有些情况下还是不够智能,在测试项目中 Assert 的时候就不能很好的工作,来看一个示例:

从上面的示例来看,在使用 importedList[i].Id/Title 之前已经使用了 Assert.NotNull(importedList[i]),理论上来说 importedList[i] 是不会为 null 的,但是编译器现在还没这么智能,还需要进一步的优化,针对这样的情况,可以单独声明一个变量,使用 ! 来声明一个不可空的引用类型,想要禁用测试项目中的警告的话也可以设置 nullable 级别为 annotations 或者 disabled
最后想说,鉴于目前 asp.net core 正在大力采用可空引用类型,大家还是可以了解一下的
Reference
- https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references
- https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/upgrade-to-nullable-references
- https://docs.microsoft.com/en-us/dotnet/csharp/nullable-migration-strategies
- https://github.com/dotnet/aspnetcore/issues/5680
- https://github.com/WeihanLi/WeihanLi.Npoi/pull/98
- https://github.com/WeihanLi/DbTool
- https://github.com/dotnet/samples/tree/master/csharp/NullableIntroduction/NullableIntroduction
- https://stackoverflow.com/questions/54526652/when-to-null-check-arguments-with-nullable-reference-types-enabled
- https://headspring.com/2020/06/02/applying-nullable-reference-types-in-fixie/
