為你的專案啟用可空引用型別
阿新 • • 發佈:2021-01-18
# 為你的專案啟用可空引用型別
## Intro
C# 從 8.0 開始引入了可空引用型別,我們可以為專案啟用可空引用型別來藉助編譯器來幫助我們更好的處理程式碼中的空引用的處理,可以避免我們寫很多不必要 null 檢查,提高我們的效率
## Why
為什麼我們要啟用可空引用型別呢,首先我們可以看一下 asp.net core 專案,asp.net core 的專案正在大量的使用可空引用型別,詳情可以參考:
> Updating ASP.NET Core to use C# 8's nullable reference types would:
>
> 1. Help ASP.NET Core libraries avoid null reference exceptions internally. It will help us find and prevent our bugs and increase our developer productivity
> 2. 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
接著我們就來看一看如何為我們的專案啟用可空引用型別吧,微軟的文件上提供了比較詳細的說明,詳細可以參考文末的引用連結
啟用可空引用型別只需要在專案檔案中新增 `enable ` 即可,`LangVersion` 需要設定為 `8` 及以上。
`Nullable` 上下文包含了兩個上下文一個是 `Nullable annotation context`(支援 `?` 表示可為空的引用型別),一個是 `Nullable warning context`(支援編譯器針對可空引用型別的警告)
`Nullable` 上下文有 4 種配置,配置如下
- `enable`
- `warnings`
- `annotations`
- `disable`
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
首先可以看一個介面:
``` csharp
public interface IPropertyConfiguration
{
IPropertyConfiguration HasColumnTitle(string title);
IPropertyConfiguration HasColumnFormatter(string? formatter);
IPropertyConfiguration HasColumnInputFormatter(Func? formatterFunc);
}
```
來看實現:
``` csharp
internal sealed class PropertyConfiguration : PropertyConfiguration, IPropertyConfiguration
{
private readonly PropertyInfo _propertyInfo;
public PropertyConfiguration(PropertyInfo propertyInfo)
{
_propertyInfo = propertyInfo;
PropertyName = propertyInfo.Name;
ColumnTitle = propertyInfo.Name;
}
public IPropertyConfiguration HasColumnTitle(string title)
{
ColumnTitle = title ?? throw new ArgumentNullException(nameof(title));
return this;
}
public IPropertyConfiguration HasColumnFormatter(string? formatter)
{
ColumnFormatter = formatter;
return this;
}
public IPropertyConfiguration HasInputFormatter(
Func? formatterFunc)
{
InternalCache.InputFormatterFuncCache.AddOrUpdate(_propertyInfo, formatterFunc);
return this;
}
}
```
可以看到 `HasColumnTitle` 的引數中的 `title` 是不可空的引用型別,即使如此實現程式碼裡還是做了 `null` 檢查,而且可空引用型別在 `throw new ArgumentNullException()` 的時候也不會引發警告
**警告示例**:
如果賦值 `null` 給一個不可為空的引用型別時,編譯器就會給出一個警告,示例如下:
![](https://img2020.cnblogs.com/blog/489462/202101/489462-20210117211224225-847008333.png)
在往一個不可空引用型別列表裡中新增 `null` 時,編譯器也會給出一個警告:
![](https://img2020.cnblogs.com/blog/489462/202101/489462-20210117211235535-553383900.png)
如果一個可空的的引用型別變數沒有檢查 `null` 的時候,也會有警告:
![](https://img2020.cnblogs.com/blog/489462/202101/489462-20210117214748798-197572111.png)
從上圖中可以看出,使用 `var` 宣告變數的時候,會是一個可空的引用型別
## More
使用可空引用型別可以一定程度上幫助我們減少不必要的 null 檢查,但是對於類庫專案來說,該有的 null 檢查還是要有的
對於應用來說,藉助可空引用型別也可以比較清晰地瞭解,哪些地方需要檢查 null,哪些地方不需要,可以提升程式碼質量
對於 `null` 包容運算子 `!` ,可以將一個可能 `null` 的物件賦值給不可空的引用型別變數,儘量不用使用,用了這個就是自己在程式碼裡埋雷,本來不會為 `null` 的變數、屬性也會出現 `null` 的情況,如果還沒有必要的 `null` 檢查,完全是自己給自己挖坑。
但是在使用過程中,感覺有些情況下還是不夠智慧,在測試專案中 `Assert` 的時候就不能很好的工作,來看一個示例:
![image-20210117223138360](C:\Users\Weiha\AppData\Roaming\Typora\typora-user-images\image-20210117223138360.png)
從上面的示例來看,在使用 `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