可為 Null 的參考型別

在 c # 8.0 之前,所有參考型別都可為 null。 可 為 null 的參考型別 是指 c # 8.0 中引進的一組功能,可讓您用來將程式碼造成執行時間擲回的可能性降至最低 System.NullReferenceException可為 null 的參考 型別包含三個可協助您避免這些例外狀況的功能,包括將參考型別明確標示為 可為 null 的功能:

  • 改善的靜態流程分析,可判斷變數是否可能 null 在取值之前。
  • 批註 Api 的屬性,讓流程分析判斷 null 狀態
  • 開發人員用來明確宣告變數之預期 null 狀態 的變數注釋。

針對現有的專案,預設會停用 Null 狀態分析和變數注釋, — 這表示所有的參考型別都會繼續成為可為 null。 從 .NET 6 開始,針對 專案預設會啟用它們。 如需宣告 可為 null 注釋內容 來啟用這些功能的詳細資訊,請參閱 可為 null 的上下文

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

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

當您取值為的變數時,執行時間會擲回 null System.NullReferenceException

Null 狀態分析

*Null 狀態分析 _ 會追蹤參考的 _null 狀態 *。 當您的程式碼可能會進行取值時,此靜態分析會發出警告 null 。 當執行時間擲回時,您可以解決這些警告以將機率降至最低 System.NullReferenceException 。 編譯器會使用靜態分析來判斷變數的 null 狀態 。 變數不可以是 null 或可能是 -null。 編譯器會以兩種方式判斷變數 不是 null

  1. 變數已指派給已知為 非 null 的值。
  2. 變數已經過檢查, null 而且自該檢查以來未曾修改過。

編譯器尚未判斷為非 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 存取之前,以及在傳遞 current 至動作之前,會先檢查變數 ProcessNode 。 先前的範例顯示當初始化、指派或比較時,編譯器如何判斷本機變數的 null 狀態 null

注意

C # 10 中新增了一些明確指派和 null 狀態分析的改進。 當您升級至 c # 10 時,您可能會發現較少的可為 null 的警告,也就是誤報。 您可以深入瞭解 明確指派改進功能規格的增強功能。

API 簽章上的屬性

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

public void PrintMessage(string message)
{
    if (!string.IsNullOrWhiteSpace(message))
    {
        Console.WriteLine($"{DateTime.Now}: {message}");
    }
}

根據檢查,任何開發人員都會將此程式碼視為安全的,且不應該產生警告。 編譯器不知道會 IsNullOrWhiteSpace 提供 null 檢查。 您可以套用屬性來通知編譯器,只有在傳回時才 message 會傳回 非 null IsNullOrWhiteSpace false 。 在上述範例中,簽章包含, NotNullWhen 表示的 null 狀態 message

public static bool IsNullOrWhiteSpace([NotNullWhen(false)] string message);

屬性提供有關引數的 null 狀態、傳回值,以及用來叫用成員之物件實例成員的詳細資訊。 在可 為 null 參考屬性的語言參考文章中,可以找到每個屬性的詳細資料。 .Net 執行時間 Api 都已在 .NET 5 中批註。 您可以藉由批註 Api 來改善靜態分析,以提供有關引數和傳回值之 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。 任何可能一開始的參考變數 null 都具有 null 狀態 ,可能是 -null

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

string? name;

? 附加至類型名稱的任何變數都是 不可為 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

泛型

泛型需要處理 T? 任何型別參數的詳細規則 T 。 規則的詳細記錄與可為 null 的實值型別及可為 null 的參考型別的不同實作為。 可為 null 的實值型別會使用結構來執行 System.Nullable<T>可為 null 的參考 型別會實作為可提供語義規則給編譯器的型別注釋。

在 c # 8.0 中,使用 T? 而不 T 會限定為 structclass 不會編譯。 這可讓編譯器 T? 清楚地解讀。 這項限制已在 c # 9.0 中移除,方法是針對不受限制的類型參數定義下列規則 T

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

針對傳回值,相當於 T? [MaybeNull]T ; 對於引數值,相當 T?[AllowNull]T 。 如需詳細資訊,請參閱語言參考中 有關 null 狀態分析的屬性 文章。

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

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

這些條件約束有助於將使用方式的詳細資訊提供給編譯器 T 。 這可協助開發人員選擇的類型 T ,並在使用泛型型別的實例時提供更好的 null 狀態 分析。

可為 Null 內容

在現有程式碼基底中開啟時,防止擲回的新功能可能 System.NullReferenceException 會造成干擾:

  • 所有明確類型的參考變數都會轉譯為不可為 null 的參考型別。
  • class泛型條件約束的意義變更為表示不可為 null 的參考型別。
  • 因為這些新規則而產生新的警告。

您必須明確地選擇在現有的專案中使用這些功能。 這會提供遷移路徑,並保留回溯相容性。 可為 Null 內容可讓您對編譯器解譯參考型別變數的方式進行細部控制。 可為 null 注釋內容 會決定編譯器的行為。 可為 null 注釋內容 有四個值:

  • disabled:編譯器的行為類似于 c # 7.3 及更早版本:
    • 停用可為 null 的警告。
    • 所有的參考型別變數都可以是可為 null 的參考型別。
    • 您無法使用類型上的尾碼將變數宣告為可為 null 的參考型別 ?
    • 您可以使用 null 容許運算子, ! 但它沒有任何作用。
  • enabled:編譯器會啟用所有 null 參考分析和所有語言功能。
    • 所有新的可為 null 警告都會啟用。
    • 您可以使用 ? 尾碼來宣告可為 null 的參考型別。
    • 所有其他參考型別變數都是不可為 null 的參考型別。
    • Null 容許運算子會抑制可能指派給的警告 null
  • 警告:編譯器會執行所有 null 分析,並在程式碼可能取值時發出警告 null
    • 所有新的可為 null 警告都會啟用。
    • 使用 ? 尾碼來宣告可為 null 的參考型別會產生警告。
    • 所有的參考型別變數都允許為 null。 不過,除非以後置詞宣告,否則成員在所有方法的左大括弧中都會有 null 狀態 的非 null ?
    • 您可以使用 null 容許運算子 !
  • 注釋:編譯器不會執行 null 分析,或在程式碼可能取值時發出警告 null
    • 所有新的可為 null 警告都已停用。
    • 您可以使用 ? 尾碼來宣告可為 null 的參考型別。
    • 所有其他參考型別變數都是不可為 null 的參考型別。
    • 您可以使用 null 容許運算子, ! 但它沒有任何作用。

您可以使用 .csproj 檔案中的 <Nullable> 元素,為專案設定可為 null 的注釋內容和可為 null 的警告內容。 這個元素會設定編譯器如何解讀型別的可 null 性,以及所發出的警告。 下表顯示允許的值,並摘要說明所指定的內容。

Context 取值警告 指派警告 參考型別 ? 尾碼 ! 運算元
disabled 已停用 已停用 全部都可為 null 無法使用 沒有任何作用
enabled 啟用 啟用 不可為 null,除非使用宣告 ? 宣告可為 null 的類型 抑制可能指派的警告 null
warnings 啟用 不適用 所有都可為 null,但在方法的左大括弧中,成員視為 非 null 產生警告 抑制可能指派的警告 null
annotations 已停用 已停用 不可為 null,除非使用宣告 ? 宣告可為 null 的類型 沒有任何作用

在 c # 8 之前編譯的程式碼中的參考型別變數,或在 已停用 內容中的參考型別變數 可以 您可以將 null值或可能為 null 的變數指派給可為 null 的 無警示 變數。 不過, 可為 null 無警示 變數的預設狀態不是 -null

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

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

範例

<Nullable>enable</Nullable>

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

  • #nullable enable:將可為 null 注釋內容和可為 null 的警告內容設定為 已啟用
  • #nullable disable:將可為 null 注釋內容和可為 null 警告內容設定為 停用
  • #nullable restore:將可為 null 注釋內容和可為 null 警告內容還原至專案設定。
  • #nullable disable warnings:將可為 null 的警告內容設定為 停用
  • #nullable enable warnings:將可為 null 的警告內容設定為 已啟用
  • #nullable restore warnings:將可為 null 警告內容還原至專案設定。
  • #nullable disable annotations:將可為 null 注釋內容設定為 停用
  • #nullable enable annotations:將可為 null 注釋內容設定為 已啟用
  • #nullable restore annotations:將批註警告內容還原至專案設定。

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

警告內容 注釋內容 使用
專案預設值 專案預設值 Default
已啟用 disabled 修正分析警告
已啟用 專案預設值 修正分析警告
專案預設值 已啟用 新增類型注釋
已啟用 已啟用 已遷移的程式碼
disabled 已啟用 在修正警告之前為程式碼加上批註
disabled disabled 將舊版程式碼加入至已遷移的專案
專案預設值 disabled 很少
disabled 專案預設值 很少

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

重要

全域可為 null 的內容不適用於產生的程式碼檔案。 在任一策略下,任何標示為已產生的原始程式檔都會 停用 可為 null 的內容。 這表示產生的檔案中的任何 Api 都不會加上批註。 有四種方式可將檔案標示為已產生:

  1. 在 editorconfig 中,指定 generated_code = true 套用至該檔案的區段。
  2. <auto-generated> 檔案 <auto-generated/> 頂端的批註中放入或。 它可以位於批註中的任何一行,但批註區塊必須是檔案中的第一個元素。
  3. 使用 TemporaryGeneratedFile_ 開始檔案名
  4. .cs..cs、 g.g.. .cs 結尾的檔案名結尾。 .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);
}

在上述範例中,在 PrintStudent(default) 不可為 null 的參考型別 FirstNameLastName 都是 null 時,不會出現警告。

另一個更常見的情況是當您處理泛型結構時。 請考慮下列範例:

#nullable enable

public struct Foo<T>
{
    public T Bar { get; set; }
}

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

在上述範例中,屬性 Barnull 在執行時間,而且會指派給不可為 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 會被取值,導致執行時間例外狀況。

另請參閱