C# 中的 null 包容運算符 “!” —— 概念、由來、用法和注意事項

在 2020 年的最後一天,博客園發起了一個開源項目:基於 .NET 的博客引擎 fluss,我抽空把源碼下載下來看了下,發現在屬性的定義中,有很多地方都用到了 null!,如下圖所示:

這是什麼用法呢?之前沒有在項目中用過,所以得空就研究了一下。

以前,! 運算符用來表示 “否”,比如不等於 !=。在 C# 8.0 以後,! 運算符有了一個新意義—— null 包容運算符,用來控制類型的可空性。要了解 null 包容運算符,首先就要了解可爲 null 的引用類型

可爲 null 的引用類型

C# 8.0 引入了可爲 null 的引用類型,與可空類型補充值類型的方式一樣,它們以相同的方式補充引用類型。也就是說,通過將 ? 追加到某引用類型,可以將變量聲明爲可以爲 null 的引用類型。例如,string? 表示可以爲 null 的 string。使用這些新類型可以更清楚地表達代碼設計的意圖 —— 比如將某些變量聲明爲 必須始終具有值,而其他一些變量聲明爲 可以缺少值

藉助這個定義,我們在定義引用類型的變量或屬性時,便有了兩種選擇:

  1. 假定引用不可以爲 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 安全性時,一個變量可以有兩種狀態:

    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; 賦值正常,沒有警告。

  • Non-Nullable:string y;

    • y 是引用類型,因此默認是不可以爲 null 的

    • y = null; 賦值會產生一個警告,因爲您給一個聲明爲不支持 null 的變量分配了一個 null 值。

    如下圖:

    ! 運算符的用法

    string x;
    string? y = null;
    
    1. x = y;

    • 非法!警告:將 null 文本或可能的 null 值轉換爲不可爲 null 類型(y 可能爲 null)。

    • 賦值運算符 = 左邊是不可以爲 null 的,但右邊是可以爲 null 的

  • x = y!;

    • 合法!

    • 賦值運算符 = 左右兩邊都是不可以爲 null 的

    • 因爲 y! 使用了 ! 運算符到 y,使得右邊也變成了不可以爲 null 的,所以賦值沒有問題。

    如下圖:

    ⚠️ 警告: null 包容運算符 ! 僅在類型系統級別關閉編譯器檢查;在運行時,該值仍然可能是 null

    這是反模式的

    C# 編程時應該儘量避免使用 null 包容運算符 !

    有一些有效的使用場景(在下面會介紹),比如單元測試,使用這個運算符是適合的。不過,在 99% 的情況下,使用替代解決方案會更好。請不要只是爲了取消警告,而在代碼中打幾十個 !。要想清楚您的場景是否真的值得使用它。

    ???? 可以使用,但要小心使用。如果沒有實際的目的或使用場景,請不要使用它。

    null 包容運算符 ! 抵消了您獲得的編譯器保證的 null 安全性的作用!

    使用 ! 運算符將導致很難發現 bug。如果您定義了一個標記爲不可以爲 null 的屬性,您也就假定了可以安全地使用它。但是在運行時,您卻突然遇到 NullReferenceException 異常而撓頭,因爲一個值在用 ! 繞過了編譯器檢查後,實際上卻變成了 null,這不是給自己添麻煩嗎?

    既然這樣,那麼,

    爲什麼 ! 運算符會存在?

    • 在某些邊緣情況下,編譯器無法檢測到可以爲 null 的值實際上是不爲 null 的。

    • 使遺留代碼庫遷移更容易。

    • 在某些情況下,您根本不關心某些內容是否爲 null

    • 在進行單元測試時,您可能想要檢查傳遞 null 時的代碼行爲。

    接下來,我們繼續看下:

    null! 是什麼意思呢?

    null! 是在告訴編譯器 null 不是 null 值,這聽起來很怪異,是不是?

    實際上,它和上面例子中的 y! 一樣。它只是看起來挺怪異,因爲您將該運算符用在了 null 字面量上,但概念是一樣的。

    我們再來看一下文章開頭提到的 fluss 源碼中的一行代碼:

    /// <summary>
    /// 所屬的博客。
    /// </summary>
    public BlogSite BlogSite { get; set; } = null!;
    

    這行代碼定義了一個名稱爲 BlogSite、類型爲 BlogSite 的不可以爲 null 的類屬性。因爲它是不可以爲 null 的,因此單從技術上講,很明顯它是不可以被賦值爲 null的。

    但是,您可以通過使用 ! 運算符,將 BlogSite 屬性賦值爲 null。因爲,就編譯器所關心的 null 安全性而言,null! 不是 null

    總結

    看到這裏,想必您肯定已經明白了 null! 是什麼意思,也學會了 null 包容運算符 ! 的概念、由來和用法。但是正如我在文中提到的那樣,編程時應該儘量避免使用 !,因爲它抵消了您本可以獲得的編譯器保證的 null 安全性;而且,這種寫法閱讀起來有點讓人費解。


    參考:

    • https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references

    • https://stackoverflow.com/questions/54724304/what-does-null-statement-mean

    • https://www.cnblogs.com/cmt/p/14217355.html

    • https://www.meziantou.net/csharp-8-nullable-reference-types.htm

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章