為你的項目啟用可空引用類型
為你的項目啟用可空引用類型
Intro
C# 從 8.0 開始引入了可空引用類型,我們可以為項目啟用可空引用類型來藉助編譯器來幫助我們更好的處理程式碼中的空引用的處理,可以避免我們寫很多不必要 null 檢查,提高我們的效率
Why
為什麼我們要啟用可空引用類型呢,首先我們可以看一下 asp.net core 項目,asp.net core 的項目正在大量的使用可空引用類型,詳情可以參考:
//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 的,但是編譯器現在還沒這麼智慧,還需要進一步的優化,Resharper 是可以檢測出來的,對於測試項目你如果遇到這種情況可以禁用可空引用類型或者使用 annotations 配置
最近改了兩個項目,開始使用了可空引用類型,有需要可以參考一下,一個是類庫項目,把原來使用 Jetbrains.Annotations 替換成了可空引用類型,可以參考://github.com/WeihanLi/WeihanLi.Npoi ,另一個是一個應用項目,可以參考://github.com/WeihanLi/DbTool/
最後想說,鑒於目前 asp.net core 正在大力採用可空引用類型,大家還是可以了解一下的
Reference
- //docs.microsoft.com/en-us/dotnet/csharp/nullable-references
- //docs.microsoft.com/en-us/dotnet/csharp/tutorials/upgrade-to-nullable-references
- //docs.microsoft.com/en-us/dotnet/csharp/nullable-migration-strategies
- //github.com/dotnet/aspnetcore/issues/5680
- //github.com/WeihanLi/WeihanLi.Npoi/pull/98
- //github.com/WeihanLi/DbTool
- //github.com/dotnet/samples/tree/master/csharp/NullableIntroduction/NullableIntroduction
- //stackoverflow.com/questions/54526652/when-to-null-check-arguments-with-nullable-reference-types-enabled
- //headspring.com/2020/06/02/applying-nullable-reference-types-in-fixie/

