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