表達意圖

已完成

在上一個單元中,您已了解 C# 編譯器如何執行靜態分析,以協助防範 NullReferenceException。 您也已了解如何啟用可為 Null 的內容。 在此單元中,您將深入了解如何在可為 Null 的內容中明確表達意圖。

宣告變數

啟用可為 Null 的內容後,您可以進一步了解編譯器如何觀察您的程式碼。 您可以根據已啟用 Null 內容產生的警告採取行動,而且這麼做會明確定義您的意圖。 例如,讓我們繼續檢查 FooBar 程式碼,並檢查宣告和指派:

// Define as nullable
FooBar? fooBar = null;

請注意 ? 已新增至 FooBar。 這會告知編譯器您明確希望 fooBar 可為 Null。 如果您不想讓 fooBar 可為 Null,但仍想要避免警告,請考慮下列事項:

// Define as non-nullable, but tell compiler to ignore warning
// Same as FooBar fooBar = default!;
FooBar fooBar = null!;

本範例會將 null-forgiving (!) 運算子新增至 null,指示編譯器將這個變數明確初始化為 null。 編譯器將不會發出有關此參考為 Null 的警告。

最好是在宣告不可為 Null 的變數時,指派非 Null 變數非 null 值 (如果可能):

// Define as non-nullable, assign using 'new' keyword
FooBar fooBar = new(Id: 1, Name: "Foo");

操作員

如上一單元中所述,C# 會定義數個運算子,以在可為 Null 的參考型別周圍表達您的意圖。

null-forgiving (!) 運算子

您已在上一節中了解 null-forgiving 運算子 (!)。 該運算子會告知編譯器忽略 CS8600 警告。 這是告知編譯器您知道自己在做什麼的其中一種方式,但有一點需要注意,您必須真的知道自己在做什麼

當您在初始化不可為 Null 的類型,並已啟用可為 Null 的內容時,可能需要明確要求編譯器進行 forgiveness。 例如,請考慮下列程式碼:

#nullable enable

using System.Collections.Generic;

var fooList = new List<FooBar>
{
    new(Id: 1, Name: "Foo"),
    new(Id: 2, Name: "Bar")
};

FooBar fooBar = fooList.Find(f => f.Name == "Bar");

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

在上述範例中,FooBar fooBar = fooList.Find(f => f.Name == "Bar"); 會產生 CS8600 警告,因為 Find 可能會傳回 null。 這個可能的 null 會指派給 fooBar,此內容不可為 Null。 不過,在此嘗試的範例中,我們知道 Find 永遠不會以寫入方式傳回 null。 您可以使用 null-forgiving 運算子向編譯器表達此意圖:

FooBar fooBar = fooList.Find(f => f.Name == "Bar")!;

請注意 ! 位於 fooList.Find(f => f.Name == "Bar") 的結尾處。 這會告知編譯器您知道 Find 方法傳回的物件可能是 null,而且沒有問題。

在方法呼叫或屬性評估之前,也可以將 null-forgiving 運算子套用至内嵌物件。 考慮其他嘗試的範例:

List<FooBar>? fooList = FooListFactory.GetFooList();

// Declare variable and assign it as null.
FooBar fooBar = fooList.Find(f => f.Name == "Bar")!; // generates warning

static class FooListFactory
{
    public static List<FooBar>? GetFooList() =>
        new List<FooBar>
        {
            new(Id: 1, Name: "Foo"),
            new(Id: 2, Name: "Bar")
        };
}

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

在前述範例中:

  • GetFooList 是靜態方法,會傳回可為 Null 的型別。List<FooBar>?
  • fooList 會獲派 GetFooList 所傳回的值。
  • 編譯器會在 fooList.Find(f => f.Name == "Bar"); 上產生警告,因為指派給 fooList 的值可能是 null
  • 假設 fooList 不是 nullFind 可能會傳回 null,但我們知道不會傳回,因此會套用 null-forgiving 運算子。

您可以將 null-forgiving 運算子套用至 fooList 以停用警告:

FooBar fooBar = fooList!.Find(f => f.Name == "Bar")!;

注意

您應該謹慎地使用 Null 表示運算子。 若使用其關閉警告,表示您告知編譯器不需協助您探索可能的 Null 錯誤。 請謹慎使用,並只有在您確定時才使用。

如需詳細資訊,請參閱 ! (null-forgiving) 運算子 (C# 參考)

Null 聯合 (??) 運算子

使用可為 Null 的型別時,您可能需要評估其目前是否為 null,並採取特定動作。 例如,當可為 Null 的類型已指派為 null 或未初始化時,您可能需要指派非 Null 值。 這就是 Null 聯合運算子 (??) 有用之處。

請考慮下列範例:

public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
    salesTax ??= DefaultStateSalesTax.Value;

    // Safely use salesTax object.
}

在上述 C# 程式碼中:

  • 參數 salesTax 定義為可為 Null 的 IStateSalesTax
  • 在方法主體中,salesTax 會使用 Null 聯合運算子有條件地指派。
    • 這可確保當 salesTax 作為 null 傳入時會有值。

提示

這項功能相當於下列 C# 程式碼:

public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
    if (salesTax is null)
    {
        salesTax = DefaultStateSalesTax.Value;
    }

    // Safely use salesTax object.
}

以下是另一個常見 C# 慣用語的範例,其中 Null 聯合運算子可能很有用:

public sealed class Wrapper<T> where T : new()
{
    private T _source;

    // If given a source, wrap it. Otherwise, wrap a new source:
    public Wrapper(T source = null) => _source = source ?? new T();
}

在上述 C# 程式碼中:

  • 定義泛型包裝函式類別,其中泛型型別參數受限於 new()
  • 建構函式接受預設為 null 的參數 T source
  • 包裝的 _source 會有條件地初始化為 new T()

如需詳細資訊,請參閱 ?? 和 ??= 運算子 (C# 參考)

Null 條件運算子 (?.)

使用可為 Null 的型別時,您可能需要根據 null 物件的狀態有條件地執行動作。 例如,在上一個單元中,FooBar 記錄是用來藉由取值 null 來示範 NullReferenceException。 這是在呼叫其 ToString 時所造成。 請考慮這個相同的範例,但現在套用 Null 條件運算子:

using System;

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

// Conditionally dereference variable.
var str = fooBar?.ToString();
Console.Write(str);

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

在上述 C# 程式碼中:

  • 有條件地取值 fooBar,並將 ToString 的結果指派給 str 變數。
    • 變數 str 的類型 string? (可為 Null 的字串)。
  • 其會將 str 的值寫入標準輸出中,但不會有任何內容。
  • 呼叫 Console.Write(null) 是有效的,因此沒有警告。
  • 如果您要呼叫 Console.Write(str.Length),則可能會收到警告,因為您可能會取值 null。

提示

這項功能相當於下列 C# 程式碼:

using System;

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

// Conditionally dereference variable.
string str = (fooBar is not null) ? fooBar.ToString() : default;
Console.Write(str);

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

您可以結合運算子來進一步表達您的意圖。 例如,您可以鏈結 ?.?? 運算子:

FooBar fooBar = null;
var str = fooBar?.ToString() ?? "unknown";
Console.Write(str); // output: unknown

如需詳細資訊,請參閱 ?. 和 ?[] (null 條件式) 運算子

摘要

在本單元中,您已了解如何在程式碼中表達 Null 屬性意圖。 在下一個單元中,您會將學到的內容套用至現有的專案。

檢定您的知識

1.

參考型別 stringdefault 值為何?

2.

取值 null 的預期行為為何?

3.

執行此 throw null; C# 程式碼時會發生什麼情況?

4.

關於可為 Null 的參考型別,哪一個陳述最精確?