C# 中的 null 包容運算子 “!” —— 概念、由來、用法和注意事項
阿新 • • 發佈:2021-01-12
在 2020 年的最後一天,[部落格園發起了一個開源專案:基於 .NET 的部落格引擎 fluss](https://www.cnblogs.com/cmt/p/14217355.html),我抽空把原始碼下載下來看了下,發現在屬性的定義中,有很多地方都用到了 `null!`,[如下圖所示](https://github.com/cnblogs/fluss/blob/main/src/Cnblogs.Fluss.Domain/Entities/ContentBlock.cs):
![cnblog null](https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010217986-574857447.png)
這是什麼用法呢?之前沒有在專案中用過,所以得空就研究了一下。
以前,`!` 運算子用來表示 “否”,比如不等於 `!=`。在 C# 8.0 以後,`!` 運算子有了一個新意義—— **`null` 包容運算子**,用來控制型別的*可空性*。要了解 `null` 包容運算子,首先就要了解*可為 null 的引用型別*。
## 可為 null 的引用型別
C# 8.0 引入了[可為 null 的引用型別](https://docs.microsoft.com/zh-cn/dotnet/csharp/nullable-references),與可空型別補充*值型別*的方式一樣,它們以相同的方式補充*引用型別*。也就是說,通過將 `?` 追加到某引用型別,可以將變數宣告為*可以為 null 的引用型別*。 例如,`string?` 表示*可以為 `null` 的 `string`*。使用這些新型別可以更清楚地表達程式碼設計的意圖 —— 比如將某些變數宣告為 **必須始終具有值**,而其他一些變數宣告為 **可以缺少值**。
藉助這個定義,我們在定義引用型別的變數或屬性時,便有了兩種選擇:
1. **假定引用不可以為 `null`。** 當變數定義為不可以為 `null` 時,編譯器會強制執行規則——確保在不檢查它們是否為 `null` 的前提下,取消引用這些變數是安全的:
- 變數必須初始化為非 `null` 值。
- 變數永遠不能賦值為 `null`。
2. **假定引用可以為 `null`。** 當變數定義為可以為 `null` 時,編譯器會強制執行不同的規則——確保您自己已正確檢查 `null` 引用:
- 只有當編譯器可以保證該值不為 `null` 時,才可以取消引用該變數。
- 這些變數可以用預設的 `null` 值進行初始化,也可以在其他程式碼中賦值為 `null`。
與 C# 8.0 之前對引用變數的處理相比,這個新功能提供了顯著的優勢。在早期版本中,不能通過變數的宣告來確定設計意圖,編譯器沒有為引用型別提供針對 `null` 引用異常的安全性。
通過新增*可為 `null` 的引用型別*,您可以更清楚地宣告您的意圖。`null` 值是表示一個變數不引用值的正確方法,請不要使用此功能從程式碼中刪除所有的 `null` 值。而是,應向編譯器和閱讀程式碼的其他開發人員宣告您的意圖。通過宣告意圖,編譯器會在您編寫與該意圖不一致的程式碼時警告您。
是不是讀起來有點繞?還是直接看示例比較容易理解些,請繼續往下看。首先,我們來
## 啟用可為 null 的引用型別
有三種方法可以啟用*可為 null 的引用型別*。
### 在專案檔案中啟用
```xml
enable
```
將上面這一行新增到專案檔案中,為當前專案啟用 *可為 null 的引用型別*,如下圖所示:
![nullable enable 1](https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010311211-976904443.png)
### 在自定義專案屬性中啟用
在 `Directory.Build.props` 檔案中可以為目錄下的所有專案啟用 *可為 null 的引用型別*, 下面截圖是 [fluss 專案中的設定](https://github.com/cnblogs/fluss/blob/main/Directory.Build.props):
![cnblog nullable enable 3](https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010354652-646106571.png)
### 使用前處理器指令啟用
可以使用 `#nullable enable` 和 `#nullable disable` 前處理器指令在程式碼中的任意位置啟用和禁用 *可為 null 的引用型別*:
![nullable enable 2](https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010506565-878112687.png)
## 舉例說明
### 典型用法
假設有這個定義:
```csharp
class Person
{
public string? MiddleName;
}
```
如下這樣呼叫:
```csharp
void LogPerson(Person person)
{
Console.WriteLine(person.MiddleName.Length); // 警告 CS8602 解引用可能出現空引用。
Console.WriteLine(person.MiddleName!.Length); // 沒有警告
}
```
![nullable enable warning](https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010556234-839926700.png)
這個 `!` 運算子基本上就是關閉了編譯器的空檢查。
### 內部執行機制
使用此運算子*告訴編譯器可以安全地訪問可能為 `null` 的內容*。您可以用它來表達在這種情況下“不關心” `null` 安全性。
當我們討論到 `null` 安全性時,一個變數可以有兩種狀態:
1. Nullable : 可以為 `null`。
2. Non-Nullable :不可以為 `null`。
從 C# 8.0 開始,所有的*引用型別*預設都是 *Non-nullable*。
“可空性”可以通過以下兩個新的**型別運算子**進行修改:
1. `!` :從 Nullable 改為 Non-Nullable
2. `?` :從 Non-Nullable 改為 Nullable
這兩個運算子是相互對應的。您使用這兩個運算子限定變數,然後編譯器根據您的限定來確保 `null` 安全性。
### `?` 運算子的用法
1. Nullable:`string? x;`
- `x` 是引用型別,因此預設是*不可以為 `null` 的*。
- 我們使用 `?` 運算子將其改為*可以為 `null` 的*。
- `x = null;` 賦值正常,沒有警告。
2. Non-Nullable:`string y;`
- `y` 是引用型別,因此預設是*不可以為 `null` 的*。
- `y = null;` 賦值會產生一個警告,因為您給一個宣告為不支援 `null` 的變數分配了一個 `null` 值。
如下圖:
![nullable enable warning y](https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010638802-823892295.png)
### `!` 運算子的用法
```csharp
string x;
string? y = null;
```
1. `x = y;`
- 非法!警告:將 null 文字或可能的 null 值轉換為不可為 null 型別(`y` 可能為 `null`)。
- 賦值運算子 `=` 左邊是*不可以為 `null` 的*,但右邊是*可以為 `null` 的*。
2. `x = y!;`
- 合法!
- 賦值運算子 `=` 左右兩邊都是*不可以為 `null` 的*。
- 因為 `y!` 使用了 `!` 運算子到 `y`,使得右邊也變成了*不可以為 `null` 的*,所以賦值沒有問題。
如下圖:
![nullable enable warning y 2](https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010716733-160026290.png)
> ⚠️ **警告:** `null` 包容運算子 `!` 僅在型別系統級別關閉編譯器檢查;在執行時,該值仍然可能是 `null`。
## 這是反模式的
**C# 程式設計時應該儘量避免使用 *`null` 包容運算子 `!`*。**
有一些有效的使用場景(在下面會介紹),比如單元測試,使用這個運算子是適合的。不過,在 99% 的情況下,使用替代解決方案會更好。請不要只是為了取消警告,而在程式碼中打幾十個 `!`。要想清楚您的場景是否真的值得使用它。
>