了解可 Null 性

已完成

如果您是 .NET 開發人員,您可能會遇到 System.NullReferenceException。 這是在執行階段取值 null,也就是在執行階段評估變數時發生,但變數會參考 null。 此例外狀況到目前為止是 .NET 生態系統內最常發生的例外狀況。 null 的建立者 (Sir Tony Hoare) 將 null 稱為「代價數十億的錯誤」。

在下列範例中,FooBar 變數會指派給 null 並立即取值,因而出現問題:

// Declare variable and assign it as null.
FooBar fooBar = null;

// Dereference variable by calling ToString.
// This will throw a NullReferenceException.
_ = fooBar.ToString();

// The FooBar type definition.
record FooBar(int Id, string Name);

當您應用程式的大小和複雜度增加時,開發人員會變得更難以找出問題。 找出此類潛在錯誤是一項工具作業,而 C# 編譯器在這裡可提供協助。

定義 Null 安全性

「Null 安全性」一詞會定義一組專屬於可為 Null 的類型特徵,以協助減少可能的 NullReferenceException 發生次數。

考慮上述 FooBar 範例,您可以藉由檢查 fooBar 變數在取值之前是否為 null 來避免 NullReferenceException

// Declare variable and assign it as null.
FooBar fooBar = null;

// Check for null
if (fooBar is not null)
{
    _ = fooBar.ToString();
}

// The FooBar type definition for example.
record FooBar(int Id, string Name);

為了協助識別此類案例,編譯器可以推斷程式碼的意圖,並強制執行所需的行為。 不過,只有在啟用 [可為 Null 的內容] 時,才會發生這種情況。 在討論 [可為 Null 的內容] 之前,讓我們先描述可能的可為 Null 的類型。

可為 Null 的類型

在 C# 2.0 之前,只有參考型別可為 Null。 intDateTime 之類的值類型「不能」null。 如果這些類型在沒有值的情況下初始化,則會回復為其 default 值。 如果是 int,則為 0。 針對 DateTime,則為 DateTime.MinValue

具現化而不具初始值的參考類型會以不同方式運作。 所有參考類型的 default 值都是 null

請考慮下列 C# 程式碼片段:

string first;                  // first is null
string second = string.Empty   // second is not null, instead it's an empty string ""
int third;                     // third is 0 because int is a value type
DateTime date;                 // date is DateTime.MinValue

在前述範例中:

  • firstnull,因為已宣告參考類型 string,但未進行指派。
  • second 在宣告時已指派 string.Empty。 物件永遠不會有 null 指派。
  • 儘管未獲指派,third 仍是 0。 其為 struct (實值類型),且具有 0default 值。
  • date 未初始化,但其 default 值為 System.DateTime.MinValue

從 C# 2.0 開始,您可以使用 Nullable<T> (或速記時使用 T?) 來定義可為 Null 的實值類型。 如此讓實值類型可為 Null。 請考慮下列 C# 程式碼片段:

int? first;            // first is implicitly null (uninitialized)
int? second = null;    // second is explicitly null
int? third = default;  // third is null as the default value for Nullable<Int32> is null
int? fourth = new();    // fourth is 0, since new calls the nullable constructor

在前述範例中:

  • firstnull,因為可為 Null 的實值型別未初始化。
  • second 在宣告時已指派 null
  • thirdnull,因為 Nullable<int>default 值是 null
  • fourth0,因為 new() 運算式會呼叫 Nullable<int> 建構函式,且依預設 int0

C# 8.0 引進「可為 Null 的參考類型」,您可以在其中表達參考類型「可能」null,或「一律」為非 null 的意圖。 您可能會認為「我認為所有參考類型都是可為 Null 的」!您沒有說錯,實際上也是如此。 這項功能可讓您表達您的意圖,而編譯器接著會嘗試強制執行。 相同的 T? 語法表示參考類型的作用是可為 Null。

請考慮下列 C# 程式碼片段:

#nullable enable

string first = string.Empty;
string second;
string? third;

假設上述範例,編譯器會推斷您的「意圖」,如下所示:

  • first「絕對不會」null,因為其已明確指派。
  • second「應該永遠不會」null,即使其一開始是 null。 指派值之前評估 second 會導致編譯器警告,因為其未初始化。
  • third「可能為」null。 例如,其「可能」會指向 System.String,但「可能」指向 null。 任一種變化都是可接受的。 如果取值 third 時未先檢查其不是 Null,則編譯器會藉由發出警告來協助您。

重要

若要使用可為 Null 的參考型別功能,如上所示,則其必須位於可為 Null 的內容中。 下一節會詳細說明這一點。

可為 Null 的內容

可為 Null 的內容可針對編譯器如何解譯參考類型變數,來啟用更細緻的控制。 可為 Null 的內容有四種可能:

  • disable:編譯器的行為類似於 C# 7.3 和更早版本。
  • enable:編譯器會啟用所有 Null 參考分析和所有語言功能。
  • warnings:編譯器會執行所有 Null 分析,並在程式碼可能取值 null 時發出警告。
  • annotations:當程式碼可能取值 null 時,編譯器不會執行 Null 分析或發出警告,但您仍然可以使用可為 Null 的參考類型 ? 和 null-forgiving 運算子 (!) 來標註程式碼。

此課程模組的範圍限於 disableenable 可為 Null 的內容。 如需詳細資訊,請參閱可為 Null 的參考類型:可為 Null 的內容

啟用可為 Null 的參考類型

在 C# 專案檔 (.csproj) 中,將子 <Nullable> 節點新增至 <Project> 元素 (或附加至現有的 <PropertyGroup>)。 這會將 enable 可為 Null 的內容套用至整個專案。

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <!-- Omitted for brevity -->

</Project>

或者,您可以使用編譯器指示詞,將「可為 Null 的內容」範圍設定為 C# 檔案。

#nullable enable

上述 C# 編譯器指示詞的功能相當於專案設定,但範圍限定於其所在的檔案。 如需詳細資訊,請參閱可為 Null 的參考型別:可為 Null 的內容 (文件)

重要

從 .NET 6.0 和更新版本開始的所有 C# 專案範本中,依預設會在 .csproj 檔案中啟用可為 Null 的內容。

啟用可為 Null 的內容時,您會收到新的警告。 請考慮上述 FooBar 範例,在可為 Null 的內容中進行分析時,會有兩個警告:

  1. FooBar fooBar = null; 行在 null 指派上有一個警告:C# 警告 CS8600:將 Null 常值或可能的 Null 值轉換為不可為 Null 的類型

    C# 警告 CS8600 的螢幕擷取畫面:正在將 Null 常值或可能的 Null 值轉換為不可為 Null 的類型。

  2. _ = fooBar.ToString(); 行也有一個警告。 這次編譯器會考慮 fooBar 可能是 Null:C# 警告 CS8602:可能為 Null 參考的取值

    C# 警告 CS8602 的螢幕擷取畫面:可能為 Null 參考的取值。

重要

即使您回應並消除所有警告,也無法「保證」Null 安全性。 某些受限的情節會傳遞編譯器的分析,但會導致執行階段 NullReferenceException

摘要

在此單元中,您已了解如何在 C# 中啟用可為 Null 的內容,以協助防範 NullReferenceException。 在下一個單元中,您將深入了解如何在可為 Null 的內容中明確表達意圖。