共用方式為


可為 Null 的參考型別

可為 Null 的參考型別 是一組功能,旨在將程式碼於執行階段引發異常的可能性降低至最低 System.NullReferenceException。 有三項功能可協助您避免這些例外狀況,包括將參考型別明確標示為可為 null 的 功能

  • 改善的靜態流程分析可判斷變數在反參考之前是否可能為 null
  • 標註 API 的屬性,讓流程分析可以判斷 Null 狀態
  • 變數註釋,開發人員用來明確宣告變數預期的 Null 狀態

編譯器會在編譯時期追蹤您程式碼中每個運算式的 Null 狀態null 狀態 有兩個值之一:

  • 「非 Null」: 已知運算式為非 null
  • 「可能為 Null」: 運算式可能是 null

變數註釋會決定參考型別變數的 可 Null 性:

  • 「不可為 Null」: 如果您將 null 值或 「可能為 Null」 運算式指派給變數,編譯程式會發出警告。 「不可為 Null」 的變數預設 Null 狀態為「不可為 Null」
  • 「可為 Null」: 您可以將 null 值或 「可能為 Null」 運算式指派給變數。 當變數的 Nll 狀態是 「可能為 Null」 時,如果您反參考變數,編譯器就會發出警告。 變數的預設 Null 狀態為 「可能為 Null」

本文的其餘部分說明這三個功能區域如何在程式代碼 取值null 值時產生警告。 反參考變數表示使用 . (點) 運算子存取其中一個成員,如下列範例所示:

string message = "Hello, World!";
int length = message.Length; // dereferencing "message"

當您反參考值為 null 的變數時,執行階段會擲回 System.NullReferenceException

同樣地,當物件為 []時,表示法可用來存取對象成員時null,可能會產生警告:

using System;

public class Collection<T>
{
    private T[] array = new T[100];
    public T this[int index]
    {
        get => array[index];
        set => array[index] = value;
    }
}

public static void Main()
{
    Collection<int> c = default;
    c[10] = 1;    // CS8602: Possible derefence of null
}

您將了解:

  • 編譯器的 Null 狀態分析: 編譯器如何判斷運算式為「非 Null」還是「可能為 Null」。
  • 套用至 API 的屬性 為編譯器的 Null 狀態分析提供更多內容。
  • 可為 Null 的變數註釋 會提供變數意圖的相關資訊。 批註適用於欄位、參數和傳回值,以設定預設 Null 狀態。
  • 管理 泛型型別引數 的規則。 新增了新的條件約束,因為型別參數可以是參考型別或實值型別。 ? 字尾會針對可為 Null 的實值型別和可為 Null 的參考型別以不同的方式實作。
  • 可為 Null 的上下文 能協助您遷移大型專案。 您可以在移轉時,在應用程式部分的可為 Null 內容中啟用警告和批注。 解決更多警告之後,您可以啟用整個專案的這兩個設定。

最後,您會了解型別與陣列中 struct Null 狀態分析的已知陷阱。

您也可以從 C# 中可 null 安全性的 Learn 課程模組,探索這些概念。

空狀態分析

Null 狀態分析 會追蹤參考的 null 狀態。 變數為 「非 Null」「可能為 Null」。 編譯器會以兩種方式判斷變數是否為不是 Null

  1. 變數已指派了一個已知非空的值
  2. 變數已針對 null 進行檢查,而且因為該檢查而未指派。

編譯程式無法判斷為非 null 的任何變數都會被視為 可能為 null。 此分析會在您不小心反參考 null 值的情況下提供警告。 編譯器會根據 Null 狀態產生警告。

  • 當變數 非空時,該變數可以安全地解參照。
  • 當變數為可能是 null 時,必須檢查該變數,以確保它在反參考之前不是 null

請考慮下列範例:

string? message = null;

// warning: dereference null.
Console.WriteLine($"The length of the message is {message.Length}");

var originalMessage = message;
message = "Hello, World!";

// No warning. Analysis determined "message" is not-null.
Console.WriteLine($"The length of the message is {message.Length}");

// warning!
Console.WriteLine(originalMessage.Length);

在上述範例中,編譯器會在列印第一則訊息時判斷 message 是否為可能是 null。 第二則訊息則不會產生警告。 最後一行程式碼會產生警告,因為 originalMessage 可能是 null。 下列範例示範更實用的用法,讓您可以周遊節點樹狀結構一直到根目錄,並在周遊期間處理每個節點:

void FindRoot(Node node, Action<Node> processNode)
{
    for (var current = node; current != null; current = current.Parent)
    {
        processNode(current);
    }
}

先前的程式碼不會針對反參考變數 current 產生任何警告。 靜態分析判斷當 current可能是 null 時永遠不會被反參考。 在存取 current 之前,以及將 null 傳遞至 current.Parent 動作之前,會檢查變數 currentProcessNode。 先前的範例示範編譯器在初始化、指派或比較 時,如何判斷區域變數的 null

Null 狀態分析不會追蹤已呼叫的方法。 因此,所有建構函式所呼叫之通用協助程式方法中初始化的欄位可能會產生具有下列訊息的警告:

結束建構函式時,不可為 Null 的屬性 'name' 必須含有不是 null 的值。

您可以使用下列兩種方式之一來解決這些警告:建構函式鏈結,或協助程式方法上的可為 Null 屬性。 下列程式碼將示範各項作業。 Person 類別使用其他所有建構函式呼叫的通用建構函式。 Student 類別具有標註 System.Diagnostics.CodeAnalysis.MemberNotNullAttribute 屬性的協助程式方法:


using System.Diagnostics.CodeAnalysis;

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public Person() : this("John", "Doe") { }
}

public class Student : Person
{
    public string Major { get; set; }

    public Student(string firstName, string lastName, string major)
        : base(firstName, lastName)
    {
        SetMajor(major);
    }

    public Student(string firstName, string lastName) :
        base(firstName, lastName)
    {
        SetMajor();
    }

    public Student()
    {
        SetMajor();
    }

    [MemberNotNull(nameof(Major))]
    private void SetMajor(string? major = default)
    {
        Major = major ?? "Undeclared";
    }
}

可為 Null 狀態分析和編譯器產生的警告可協助您藉由反參考 null 來避免程式錯誤。 解決可為 Null 警告一文中所提供的技術,會更正您可能會在程式碼中看到的警告。 從空狀態分析中產生的診斷僅為警告。

API 簽章的屬性

Null 狀態分析需要開發人員提供提示,才能了解 API 的語意。 某些 API 會提供 Null 檢查,而且應該將變數的 Null 狀態可能是 null 變更為不是 null。 其他 API 會根據輸入引數的 null 狀態,傳回不是 null可能是 null 的運算式。 例如,請考慮下列以大寫顯示訊息的程式碼:

void PrintMessageUpper(string? message)
{
    if (!IsNull(message))
    {
        Console.WriteLine($"{DateTime.Now}: {message.ToUpper()}");
    }
}

bool IsNull(string? s) => s == null;

根據檢查,任何開發人員都會考慮此程式碼安全,且不應產生警告。 不過由於 IsNullmessage.ToUpper() 變數,編譯器並不知道 message 會提供 Null 檢查並對 陳述式發出警告。 使用 NotNullWhen 屬性來修正此警告:

bool IsNull([NotNullWhen(false)] string? s) => s == null;

這個屬性會通知編譯器,如果 IsNull 傳回 false,則參數 s 不是 Null。 編譯器會將 message變更為 區塊內的 if (!IsNull(message)) {...}。 不會發出任何警告。

屬性會提供關於引數、傳回值,以及用來叫用成員的物件執行個體成員的 Null 狀態的詳細資訊。 如需每個屬性的詳細資訊,請參閱有關可為 null 參考屬性的語言參考文章。 自 .NET 5 起,所有 .NET 執行階段 API 都會加上註釋。 您可以藉由標註 API 來改善靜態分析,以提供引數和傳回值的 Null 狀態語意資訊。

可為 Null 的變數註釋

Null 狀態分析可為區域變數提供穩健的分析。 編譯器需要成員變數的其餘資訊。 編譯器需要更多資訊,才能在成員的左括弧中設定所有欄位的 Null 狀態。 任何可存取的建構函式都可以用來初始化物件。 如果成員欄位可能設定為 null,編譯器必須在每個方法的開頭假設其 null 狀態可能是 null

您可以使用註釋來宣告變數是可為 Null 的參考型別還是不可為 Null 的參考型別。 這些註釋為變數的 null 狀態提供重要說明:

  • 參考不應為 Null。 不可為 Null 參考變數的預設狀態為 「非 Null」。 編譯器會實施規則,確保對這些變數取值 (Dereference) 的過程是安全的,而不會事先檢查這些變數是不是 Null:
    • 變數必須初始化為非 Null 值。
    • 變數永遠不可指派 null 值。 當程式碼將可能是 Null 運算式指派給不應該是 Null 的變數時,編譯器會發出警告。
  • 參考可能為 Null。 可為 Null 參考變數的預設狀態為可能是 null。 編譯器會強制執行規則,以確保您已正確檢查 null 參考:
    • 只有在編譯器可以保證變數的值不是 null時,才能解引用該變數。
    • 這些變數可以使用預設 null 值初始化,而且可以在其他程式代碼中指派值 null
    • 當程式碼將 「可能是 Null」 運算式指派給可能為 Null 的變數時,編譯器不會發出警告。

任何不可為 Null 的參考變數,非 null的初始 null 狀態。 任何可為 Null 的參考變數都具有「可能是 Null」 的初始 Null 狀態

可為 Null 參考型別的語法與可為 Null 實值型別語法相同:將 ? 附加至變數的型別。 例如,下列變數宣告代表可為 Null 字串變數,name

string? name;

啟用可為 Null 的參考型別時,? 未附加至型別名稱的任何變數,都為 不可為 Null 的參考型別。 這包含您啟用此功能時現有程式碼中所有的參考型別變數。 不過,任何隱含型別區域變數 (使用 var 宣告) 都是可為 Null 的參考型別。 如前幾節所示,靜態分析會判斷區域變數的 Null 狀態,以在確認它們在反參考前是否為「可能是 Null」

有時候,當您知道變數不是 Null,但編譯器判斷其 null 狀態可能是 null 時,您必須覆寫警告。 您可以在變數名稱後面使用 null 容許運算子!,強制 null 狀態成為不是 null。 例如,若您知道 name 變數並非為 null,但編譯器卻發出警告,您可以撰寫下列程式碼來覆寫編譯器的分析:

name!.Length;

可為 Null 的參考型別和可為 Null 的實值型別提供類似的語意概念: 變數可以代表值或物件,或該變數可能是 null。 不過,可為 Null 的參考型別和可為 Null 的實值型別會以不同的方式實作:可為 Null 的實值型別是使用 System.Nullable<T> 來實作,而可為 Null 的參考型別則由編譯器讀取的屬性實作。 例如,string?string 都以相同的型別表示:System.String。 不過,int?int 分別以 System.Nullable<System.Int32>System.Int32 表示。

可為 Null 的參考型別是編譯時間功能。 這表示呼叫端可能會忽略警告,並刻意使用 null 作為預期不可為 Null 參考的方法引數。 程式庫建立者應該納入 Null 引數值的執行階段檢查。 ArgumentNullException.ThrowIfNull 是在執行階段檢查參數是否含有 null 的偏好選項。 此外,若移除所有可為 Null 的註釋(?!),則程式的運行行為保持不變。 其唯一目的是表達設計意圖,並提供 Null 狀態分析的資訊。

重要

啟用可為 Null 的註釋可以變更 Entity Framework Core 判斷是否需要資料成員的方式。 如需詳細資訊,請參閱 Entity Framework Core 基本概念:使用可為 Null 的參考型別一文。

泛型

泛型需要詳細的規則來處理任何型別參數 T? 中的 T。 因為記錄和可為 Null 實值型別和可為 Null 參考型別的不同實作方式,有必要詳細說明規則。 可為 null 實值型別的實作是使用 System.Nullable<T> 結構。 可為 null 參考型別是當成向編譯器提供語意規則的型別註釋來實作。

  • 如果 T 的型別引數是參考型別,則 T? 會參考對應的可為 Null 參考型別。 例如,如果 Tstring,則 T?string?
  • 如果 T dr11型別引數是實值型別,則 T? 會參考相同的實值型別 T。 例如,如果 Tint,則 T? 也是 int
  • 如果 T 的型別引數是可為 null 參考型別,則 T? 會參考相同的可為 Null 參考型別。 例如,如果 Tstring?,則 T? 也是 string?
  • 如果 T 的型別引數是可為 null 實值型別,則 T? 會參考相同的可為 Null 實值型別。 例如,如果 Tint?,則 T? 也是 int?

若為傳回值,T? 相當於 [MaybeNull]T;若為引數值,T? 相當於 [AllowNull]T。 如需詳細資訊,請參閱語言參考中的 Null 狀態分析的屬性一文。

您可以使用條件約束來指定不同的行為:

  • class 條件約束表示 T 必須是不可為 Null 的參考型別 (例如 string)。 如果您使用可為 Null 的參考型別,例如 string? 中的 T,編譯器會產生警告。
  • class? 條件約束表示 T 必須是參考型別,不論是不可為 Null (string) 還是可為 Null 的參考型別 (例如 string?)。 當型別參數是可為 Null 的參考型別 (例如 string?) 時,T? 的運算式會參考該相同的可為 null 參考型別,例如 string?
  • notnull 條件約束表示 T 必須是不可為 Null 的參考型別或不可為 Null 的實值型別。 如果您在型別參數中使用可為 Null 的參考型別或可為 Null 的實值型別,編譯器會產生警告。 此外,當 T 是實值型別時,傳回值就是該實值型別,而不是對應的可為 Null 實值型別。

這些限制式有助於將如何使用 T 的詳細資訊提供給編譯器。 這可協助開發人員選擇 T 的型別,並在使用泛型型別的執行個體時提供更好的 Null 狀態分析。

可空值上下文

可為 Null 的上下文 會決定如何處理可為 Null 的參考型別註釋,以及靜態 Null 狀態分析會產生哪些警告。 可空性上下文包含兩個標誌:註釋 設定和 警告 設定。

預設情況下,現有項目的 批註警告 設定都是停用的。 從 .NET 6 (C# 10 開始,預設會針對 新的 項目啟用這兩個旗標。 可為 Null 內容有兩個不同的旗標的原因是,更容易移轉在引進可為 Null 參考型別之前的大型專案。

針對小型專案,您可以啟用可為 Null 的參考型別、修正警告並繼續。 不過,對於較大的專案和多項目解決方案,可能會產生大量的警告。 您可以使用 pragmas,在開始使用可為 Null 的參考型別時,依檔案啟用可為 Null 的參考型別。 在現有的程式碼基底中開啟防止擲回 System.NullReferenceException 的新功能時,可能會發生干擾:

  • 系統會將所有型別設定明確的參考變數解譯為不可為 Null 的參考型別。
  • 泛型中 class 條件約束的意義已變更為表示不可為 Null 的參考型別。
  • 由於有這些新規則,因此產生新的警告。

可為 Null 註釋內容決定編譯器的行為。 可為 Null 的上下文 設定有四種可能的組合:

  • 兩者皆停用:程式碼 對於 null 的知覺為空停用 符合可為 Null 參考型別啟用之前的行為,但新語法會產生警告,而不是錯誤。
    • 系統會停用可為 Null 的警告。
    • 所有參考型別變數都是可為 Null 的參考型別。
    • 使用 ? 尾碼來宣告可為 Null 的參考型別會產生警告。
    • 您可以使用 null 容許運算子 !,但沒有任何作用。
  • 同時啟用:編譯器會啟用所有 null 參考分析和所有語言功能。
    • 所有新的可為 Null 警告都會啟用。
    • 您可以使用 ? 尾碼來宣告可為 Null 的參考型別。
    • 沒有 ? 尾碼的參考型別變數是不可為 Null 的參考型別。
    • Null 表示運算符會隱藏可能取值 null的警告。
  • 啟用 警告:編譯程式會執行所有空值分析,並在程式碼可能解參考 null時發出警告。
    • 所有新的可為 Null 警告都會啟用。
    • 使用 ? 尾碼來宣告可為 Null 的參考型別會產生警告。
    • 所有參考型別變數都允許為 Null。 不過,除非使用 尾碼宣告,否則成員在所有方法的左大括弧上的 null 狀態均為?
    • 您可以使用 Null 容許運算子 !
  • 啟用 批注:編譯程式不會在程式代碼 取值時發出警告,或將可能 null 表達式指派給不可為 Null 的變數時發出警告。
    • 系統會停用所有新的可為 Null 警告。
    • 您可以使用 ? 尾碼來宣告可為 Null 的參考型別。
    • 沒有 ? 尾碼的參考型別變數是不可為 Null 的參考型別。
    • 您可以使用 null 容許運算子 !,但沒有任何作用。

專案的可為 Null 註釋內容和可為 Null 警告內容都可在您的 <Nullable> 檔案中使用 元素 來設定。 此元素會設定編譯器解譯型別可 NULL 性的方式及所發出的警告。 下表顯示允許的值,並摘要說明它們所指定的內容。

上下文 反參考警告 指派警告 參考型別 ? 尾碼 ! 運算子
disable 停用 停用 全部是可為 Null 產生警告 沒有作用
enable 啟用 啟用 除非使用 ? 宣告,否則是不可為 Null 宣告可為 Null 型別 針對可能的 null 指派隱藏警告
warnings 啟用 不適用 全部是可為 Null,但在方法的左大括弧位置,成員會被視為 「非 Null」 產生警告 針對可能的 null 指派隱藏警告
annotations 停用 停用 除非使用 ? 宣告,否則是不可為 Null 宣告可為 Null 型別 沒有作用

已停用內容中,已編譯程式碼中的參考型別變數是可為 Null 遺忘型。 您可以將 null 常值或「可能是 Null」 變數指派給「可為 Null 遺忘型」的變數。 不過,可為 null 遺忘型變數的預設狀態為不是 null

您可以選擇最適合您專案的設定:

  • 針對您不想依據診斷或新功能來更新的舊版專案,請選擇 [停用]
  • 選擇 [警告] 來判斷程式碼可能擲回 System.NullReferenceException 的位置。 您可以在修改程式碼之前解決這些警告,以啟用不可為 Null 參考型別。
  • 在啟用警告之前,選擇 [註釋] 來表達您的設計意圖。
  • 針對您想要防止 Null 參考例外狀況的新專案和使用中專案,選擇 [啟用]

範例:

<Nullable>enable</Nullable>

您也可以在原始程式碼中的任何位置使用指令來設定相同的標誌。 當您移轉大型程式碼基底時,這些指示詞最有用。

  • #nullable enable:將批註和警告旗標設定為 啟用
  • #nullable disable:將批註和警告旗標設定為 停用
  • #nullable restore:將批註旗標和警告旗標還原至項目設定。
  • #nullable disable warnings:將警告旗標設定為 停用
  • #nullable enable warnings:將警告旗標設定為 啟用
  • #nullable restore warnings:將警告旗標還原至項目設定。
  • #nullable disable annotations:將批註旗標設定為 停用
  • #nullable enable annotations:將批註旗標設定為 啟用
  • #nullable restore annotations:將批註旗標還原至項目設定。

針對任何程式碼,您可以設定下列任何組合:

警告旗標 註釋旗標 使用
專案預設值 專案預設值 預設
enable disable 修正分析警告
enable 專案預設值 修正分析警告
專案預設值 enable 新增型別註釋
enable enable 程式碼已移轉
disable enable 修正警告之前標註程式碼
disable disable 將舊版程式碼新增至移轉的專案
專案預設值 disable 很少
disable 專案預設值 很少

這九種組合可讓您更精細地控制編譯器因程式碼而發出的診斷。 您可以在您要更新的任何區域中啟用更多功能,而不會看到尚未準備好要解決的其他警告。

重要

內容全域可為 null 不適用於產生的程式碼檔案。 不論採用任何一個策略,所有標記為產生的來源檔案,都會「停用」內容可為 null。 這表示產生的檔案中的所有 API,都不會有標註。 已產生的檔案不會顯示關於可為 Null 的警告。 有四種方式可將檔案標記為是產生的檔案:

  1. 在 .editorconfig 中,於套用至該檔案的區段中,指定 generated_code = true
  2. <auto-generated><auto-generated/> 置於檔案頂端的註解中。 它可以在註解的任一行,但註解區塊必須是檔案中的第一個元素。
  3. 使用 TemporaryGeneratedFile_ 做為檔案名稱的開頭
  4. 使用 .designer.cs.generated.cs.g.cs.g.i.cs 做為檔案名稱的結尾。

產生器可以選擇使用 #nullable 前置處理指示詞。

根據預設,停用可為 Null 的註釋和警告旗標 。 這表示您現有的程式碼會編譯且不會進行任何變更,也不會產生任何新的警告。 從 .NET 6 開始,新專案會在所有項目範本中包含 <Nullable>enable</Nullable> 元素,將這些旗標設定為啟用

這些選項提供兩種不同的策略,讓您可以更新現有的程式碼基底,以使用可為 Null 的參考型別。

已知錯誤

包含參考型別的陣列和結構是可為 Null 參考和靜態分析 (決定 Null 安全性) 中的已知錯誤。 在這兩種情況下,不可為 Null 的參考可能會初始化為 null,而不會產生警告。

結構

包含不可為 Null 參考型別的結構允許指派 default,而不需要任何警告。 請考慮下列範例:

using System;

#nullable enable

public struct Student
{
    public string FirstName;
    public string? MiddleName;
    public string LastName;
}

public static class Program
{
    public static void PrintStudent(Student student)
    {
        Console.WriteLine($"First name: {student.FirstName.ToUpper()}");
        Console.WriteLine($"Middle name: {student.MiddleName?.ToUpper()}");
        Console.WriteLine($"Last name: {student.LastName.ToUpper()}");
    }

    public static void Main() => PrintStudent(default);
}

在上述範例中,當不可為 Null 參考型別 PrintStudent(default)FirstName 是 null 時,LastName 沒有任何警告。

另一個較常見的案例是處理泛型結構時。 請考慮下列範例:

#nullable enable

public struct S<T>
{
    public T Prop { get; set; }
}

public static class Program
{
    public static void Main()
    {
        string s = default(S<string>).Prop;
    }
}

在上述範例中,屬性 Prop 在執行階段為 null。 它會指派給不可為 Null 的字串,而不會有任何警告。

陣列

陣列也是可為 Null 參考型別中的已知錯誤。 請考慮不會產生任何警告的下列範例:

using System;

#nullable enable

public static class Program
{
    public static void Main()
    {
        string[] values = new string[10];
        string s = values[0];
        Console.WriteLine(s.ToUpper());
    }
}

在上述範例中,陣列的宣告顯示它含有不可為 Null 的字串,而其元素全部初始化為 null。 然後,變數 s 會指派 null 值 (陣列的第一個元素)。 最後,變數 s 會反參考,造成執行階段例外狀況。

另請參閱