分享方式:


解決可為 Null 的警告

本文涵蓋下列編譯器警告:

  • CS8597 - 擲回值可能為 Null。
  • CS8600 - 正在將 Null 常值或可能的 Null 值轉換為不可為 Null 的型別。
  • CS8601 - 可能有 Null 參考指派。
  • CS8602 - 可能有 Null 參考的取值。
  • CS8603 - 可能有 Null 參考傳回。
  • CS8604 - 參數可能有 Null 參考引數。
  • CS8605 - Unboxing 可能為 Null 值。
  • CS8607 - 可能的 Null 值不能用於標示 [NotNull][DisallowNull] 的型別
  • CS8608 - 型別中參考型別的可 Null 性與覆寫的成員不符合。
  • CS8609 - 傳回型別中參考型別的可 Null 性與覆寫的成員不符合。
  • CS8610 - 參數型別中參考型別的可 Null 性與覆寫的成員不符合。
  • CS8611 - 參數型別中參考型別的可 Null 性與部分方法宣告不符合。
  • CS8612 - 型別中參考型別的可 Null 性與隱含實作的成員不符合。
  • CS8613 - 傳回型別中參考型別的可 Null 性與隱含實作的成員不符合。
  • CS8614 - 參數型別中參考型別的可 Null 性與隱含實作的成員不符合。
  • CS8615 - 型別中參考型別的可 Null 性與實作的成員不符合。
  • CS8616 - 傳回型別中參考型別的可 Null 性與實作的成員不符合。
  • CS8617 - 參數型別中參考型別的可 Null 性與實作的成員不符合。
  • CS8618 - 非 Null 變數在結束建構函式時必須包含非 Null 值。請考慮將其宣告為可 Null。
  • CS8619 - 值中參考型別的可 Null 性與目標型別不符合。
  • CS8620 - 因為參考型別的可 Null 性有所差異,所以引數無法用於參數。
  • CS8621 - 傳回型別中參考型別的可 Null 性與目標委派不相符 (可能的原因是可 Null 性屬性)。
  • CS8622 - 參數類型中參考型別的可 Null 性與目標委派不相符 (可能的原因是可 Null 性屬性)。
  • CS8624 - 因為參考型別的可 Null 性有所差異,所以引數無法作為輸出。
  • CS8625 - 無法將 Null 常值轉換成不可為 Null 的參考型別。
  • CS8629 - 可為 Null 的實值型別可能為 Null。
  • CS8631 - 類型在泛型型別或方法中不能當做類型參數使用。型別引數可 NULL 性不符合條件約束類型。
  • CS8633 - 方法的型別參數之條件約束的可 NULL 性不符合介面方法的型別參數之條件約束。請考慮改用明確的介面實作。
  • CS8634 - 類型在泛型型別或方法中不能當做型別參數使用。型別引數的可 NULL 性不符合「類別」條件約束。
  • CS8643 - 明確介面指定名稱中參考類型可 Null 性與類型所實作的介面不相符。
  • CS8644 - 類型未實作介面成員。基底類型所實作之介面中的參考類型的可 NULL 性不相符。
  • CS8645 - 成員已列在類型上的介面清單中,並具有不同的參考類型可 Null 性。
  • CS8655 - Switch 運算式不會處理某些 Null 輸入 (並不詳盡)。
  • CS8667 - 部分方法宣告在類型參數的限制式中,有不一致的可 Null 性。
  • CS8670 - 物件或集合初始設定式意味會解除參考可能為 Null 的成員。
  • CS8714 - 類型在泛型型別或方法中不能當做型別參數使用。型別引數的可 NULL 性不符合「非 NULL」條件約束。
  • CS8762 - 參數在結束時必須有非 Null 值。
  • CS8763 - 標示 [DoesNotReturn] 的方法不應傳回。
  • CS8764 - 傳回型別是否可為 Null 的情況,與覆寫的成員不相符 (可能的原因是屬性可為 Null)。
  • CS8765 - 參數類型是否可為 Null 的情況,與覆寫的成員不相符 (可能的原因是屬性可為 Null)。
  • CS8766 - 傳回型別中參考型別是否可為 Null 的情況,與隱含實作的成員不相符 (可能的原因是屬性可為 Null)。
  • CS8767 - 參數類型中參考型別是否可為 Null 的情況,與隱含實作的成員不相符 (可能的原因是屬性可為 Null)。
  • CS8768 - 傳回型別中參考型別是否可為 Null 的情況,與實作的成員不相符 (可能的原因是屬性可為 Null)。
  • CS8769 - 參數類型中參考型別是否可為 Null 的情況,與實作的成員不相符 (可能的原因是屬性可為 Null)。
  • CS8770 - 方法缺少 [DoesNotReturn] 註釋,與實作或覆寫的成員不相符。
  • CS8774 - 成員在結束時必須有非 Null 值。
  • CS8776 - 成員不可用於此屬性中。
  • CS8775 - 成員在結束時必須有非 Null 值。
  • CS8777 - 參數在結束時必須有非 Null 值。
  • CS8819 - 傳回型別中參考型別的可 Null 性與部分方法宣告不符。
  • CS8824 - 參數在結束時必須有非 Null 值,因為參數為非 Null。
  • CS8825 - 因為參數不是 Null,所以傳回值必須非 Null。
  • CS8847 - Switch 運算式不會處理某些 Null 輸入 (並不詳盡)。不過,具有『when』子句的模式可能會成功比對此值。

可為 Null 警告的目的,即是盡可能降低應用程式執行時擲回 System.NullReferenceException 的機會。 為了達成此目標,當程式碼的建構有可能會導致 Null 參考例外狀況時,編譯器會使用靜態分析和發出警告。 您可套用型別註釋和屬性,為編譯器提供靜態分析的資訊。 這些註釋和屬性描述型別引數、參數和成員的可 Null 性。 在本文中,您將了解不同的技術,以解決編譯器靜態分析產生的可為 Null 警告。 此處所述的技術適用於一般 C# 程式碼。 請參閱使用可為 Null 的參考型別,了解如何使用可為 Null 的參考型別和 Entity Framework 核心。

您可使用四種技術之一來解決幾乎所有警告:

  • 新增必要的 Null 檢查。
  • 新增 ?! 可為 Null 的註釋。
  • 新增描述 Null 語意的屬性。
  • 正確初始化變數。

Null 的可能取值

此警告組合會警示,您正在取值 Null 狀態可能為 Null 的變數。 這些警告如下:

  • CS8602 - 可能有 Null 參考的取值。
  • CS8670 - 物件或集合初始設定式意味會解除參考可能的 Null 成員。

下列程式碼示範上述警告的各個範例:

class Container
{
    public List<string>? States { get; set; }
}

internal void PossibleDereferenceNullExamples(string? message)
{
    Console.WriteLine(message.Length); // CS8602

    var c = new Container { States = { "Red", "Yellow", "Green" } }; // CS8670
}

在上述範例中,警告原因為 Containerc 可能具有 States 屬性的 Null 值。 將新狀態指派給可能為 Null 的集合時,便會導致警告。

若要移除這些警告,您必須新增程式碼,以便將該變數的 Null 狀態變更為非 Null 再取值。 集合初始設定式警告可能較難找出。 在初始設定式將元素加入集合時,編譯器會偵測該集合可能為 Null

在許多情況下,您可在取值前先檢查變數不是 Null,以修正這些警告。 請考慮在取消引用 message 參數之前新增 Null 檢查的下列專案:

void WriteMessageLength(string? message)
{
    if (message is not null)
    {
        Console.WriteLine(message.Length);
    }
    
}

下列範例會初始化 States 的備份儲存體,並移除 set 存取子。 類別的取用者可以修改集合的內容,而且集合的儲存體絕不是 null:

class Container
{
    public List<string> States { get; } = new();
}

至於其他執行個體,收到這些警告時可能為誤判。 您可能有一個測試 null 的私人公用程式方法。 編譯器不知道這項方法提供 Null 檢查。 請考慮使用私人公用程式方法 IsNotNull 的下列範例:

public void WriteMessage(string? message)
{
    if (IsNotNull(message))
        Console.WriteLine(message.Length);
}

編譯器會警告,當您撰寫屬性 message.Length 時可能會取值 null,因為其靜態分析判斷 message 可能為 null。 您可能知道 IsNotNull 提供 Null 檢查,且傳回 true 時,Null 狀態message非 Null。 以上必須告知編譯器。 其中一種方式是 Null 容許運算子 !。 您可變更 WriteLine 陳述式,以符合下列程式碼:

Console.WriteLine(message!.Length);

Null 容許運算子會讓運算式為非 Null,即使運算式可能為 Null,且不套用 !。 在此範例中,更好的解決方案是將屬性新增至 IsNotNull 的特徵標記:

private static bool IsNotNull([NotNullWhen(true)] object? obj) => obj != null;

System.Diagnostics.CodeAnalysis.NotNullWhenAttribute 會通知編譯器,當方法傳回 true 時,用於 obj 參數的引數非 Null。 當方法傳回 false 時,引數的 Null 狀態與呼叫方法前的狀態相同。

提示

您可使用一系列豐富屬性,以描述方法和屬性對 Null 狀態的影響。 如需深入了解,您可參閱語言參考文章中的可為 Null 的靜態分析屬性

修正可能為 Null 變數取值的警告時,包含三種技術之一:

  • 新增遺漏的 Null 檢查。
  • 在 API 上新增 Null 分析屬性,以影響編譯器的 Null 狀態靜態分析。 呼叫方法後,這些屬性會通知編譯器,傳回值或引數應可能為 Null非 Null
  • 將 Null 容許運算子 ! 套用至運算式,強制狀態為非 Null

已將可能的 Null 指派給不可為 Null 的參考

此警告組合會警示,您正在將不可為 Null 的變數類型指派給 Null 狀態可能為 Null 的運算式。 這些警告如下:

  • CS8597 - 擲回值可能為 Null。
  • CS8600 - 正在將 Null 常值或可能的 Null 值轉換為不可為 Null 的型別。
  • CS8601 - 可能有 Null 參考指派。
  • CS8603 - 可能有 Null 參考傳回。
  • CS8604 - 參數可能有 Null 參考引數。
  • CS8605 - Unboxing 可能為 Null 值。
  • CS8625 - 無法將 Null 常值轉換成不可為 Null 的參考型別。
  • CS8629 - 可為 Null 的實值型別可能為 Null。

當您嘗試將可能為 Null 的運算式指派給不可為 Null 的變數時,編譯器會發出這些警告。 例如:

string? TryGetMessage(int id) => "";

string msg = TryGetMessage(42);  // Possible null assignment.

不同的警告會表示程式碼的相關詳細資料,例如指派、unboxing 指派、傳回陳述式、方法的引數,以及擲回運算式。

您可採取三個動作之一來解決這些警告。 其中一個是新增 ? 註釋,讓變數成為可為 Null 的參考型別。 該變更可能會導致其他警告。 將變數從不可為 Null 的參考變更為可為 Null 的參考,其預設 Null 狀態會從非 Null 變更為可能為 Null。 編譯器靜態分析可能會找到取值的可能為 Null 變數執行個體。

其他動作會向編譯器指示,指派的右側為非 Null。 右側的運算式可在指派前檢查 Null,如下列範例所示:

string notNullMsg = TryGetMessage(42) ?? "Unknown message id: 42";

上述範例示範方法的傳回值指派。 您可標註方法 (或屬性),以表示方法傳回非 Null 值的時間。 輸入引數非 Null 時,System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute 通常會將傳回值指定為非 Null。 另一個替代方法是將 Null 容許運算子 ! 新增至右側:

string msg = TryGetMessage(42)!;

修正可能為 Null 運算式指派給非 Null 變數的警告時,包含四種技術之一:

  • 將指派的左側變更為可為 Null 的類型。 當您取值該變數時,此動作可能會產生新的警告。
  • 先提供 Null 檢查再指派。
  • 標註會產生指派右側的 API。
  • 將 Null 容許運算子新增至指派的右側。

不可為 Null 的參考未初始化

此警告組合會警示,您正在將不可為 Null 的變數類型指派給 Null 狀態可能為 Null 的運算式。 這些警告如下:

  • CS8618 - 非 Null 變數在結束建構函式時必須包含非 Null 值。請考慮將其宣告為可 Null。
  • CS8762 - 參數在結束時必須有非 Null 值。

請考慮使用下列範例類別:

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

FirstNameLastName 皆不保證初始化。 若此為新程式碼,請考慮變更公用介面。 上述範例可更新如下:

public class Person
{
    public Person(string first, string last)
    {
        FirstName = first;
        LastName = last;
    }

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

若需要在設定名稱前建立 Person 物件,您可使用預設的非 Null 值來初始化屬性:

public class Person
{
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
}

另一個可能的替代方案,則是將這些成員變更為可為 Null 的參考型別。 若名稱應允許 null,則可定義 Person 類別如下:

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

現有的程式碼可能需要進行其他變更,以通知編譯器這些成員的 Null 語意。 您可能已建立多個建構函式,且類別可能具有可初始化一或多個成員的私人協助程式方法。 您可將初始化程式碼移至單一建構函式,並確保所有建構函式皆使用共同的初始化程式碼來呼叫建構函式。 或者,您也可使用 System.Diagnostics.CodeAnalysis.MemberNotNullAttributeSystem.Diagnostics.CodeAnalysis.MemberNotNullWhenAttribute 屬性。 這些屬性會通知編譯器,呼叫方法後成員為非 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 容許運算子在其他程式碼中初始化成員。 如需其他範例,請考慮代表 Entity Framework Core 模型的下列類別:

public class TodoItem
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

public class TodoContext : DbContext
{
    public TodoContext(DbContextOptions<TodoContext> options)
        : base(options)
    {
    }

    public DbSet<TodoItem> TodoItems { get; set; } = null!;
}

DbSet 屬性會初始化為 null!。 這可告知編譯器屬性已設為非 Null 值。 事實上,基底 DbContext 會執行該集合的初始化。 編譯器的靜態分析不會挑選該集合。 如需可為 Null 參考類型和 Entity Framework Core 的詳細資訊,請參閱使用 EF Core 中可為 Null 的參考型別一文。

修正不可為 Null 成員未初始化的警告時,包含四種技術之一:

  • 變更建構函式或欄位初始設定式,以確保所有不可為 Null 的成員皆已初始化。
  • 將一或多個成員變更為可為 Null 的類型。
  • 標註任何協助程式方法,已表示指派的成員。
  • 將初始設定式新增至 null!,表示該成員在其他程式碼中初始化。

可 Null 性宣告不符

許多警告會表示方法、委派或型別參數的特徵標記可 Null 性不符。

  • CS8608 - 型別中參考型別的可 Null 性與覆寫的成員不符合。
  • CS8609 - 傳回型別中參考型別的可 Null 性與覆寫的成員不符合。
  • CS8610 - 參數型別中參考型別的可 Null 性與覆寫的成員不符合。
  • CS8611 - 參數型別中參考型別的可 Null 性與部分方法宣告不符合。
  • CS8612 - 型別中參考型別的可 Null 性與隱含實作的成員不符合。
  • CS8613 - 傳回型別中參考型別的可 Null 性與隱含實作的成員不符合。
  • CS8614 - 參數型別中參考型別的可 Null 性與隱含實作的成員不符合。
  • CS8615 - 型別中參考型別的可 Null 性與實作的成員不符合。
  • CS8616 - 傳回型別中參考型別的可 Null 性與實作的成員不符合。
  • CS8617 - 參數型別中參考型別的可 Null 性與實作的成員不符合。
  • CS8619 - 值中參考型別的可 Null 性與目標型別不符合。
  • CS8620 - 因為參考型別的可 Null 性有所差異,所以引數無法用於參數。
  • CS8621 - 傳回型別中參考型別的可 Null 性與目標委派不相符 (可能的原因是可 Null 性屬性)。
  • CS8622 - 參數類型中參考型別的可 Null 性與目標委派不相符 (可能的原因是可 Null 性屬性)。
  • CS8624 - 因為參考型別的可 Null 性有所差異,所以引數無法作為輸出。
  • CS8631 - 類型在泛型型別或方法中不能當做類型參數使用。型別引數的可 NULL 性不符合條件約束類型。
  • CS8633 - 方法的型別參數之條件約束的可 NULL 性不符合介面方法的型別參數之條件約束。請考慮改用明確的介面實作。
  • CS8634 - 類型在泛型型別或方法中不能當做型別參數使用。型別引數的可 NULL 性不符合「類別」條件約束。
  • CS8643 - 明確介面指定名稱中參考類型可 Null 性與類型所實作的介面不相符。
  • CS8644 - 類型未實作介面成員。基底類型所實作之介面中的參考類型的可 NULL 性不相符。
  • CS8645 - 成員已列在類型上的介面清單中,並具有不同的參考類型可 Null 性。
  • CS8667 - 部分方法宣告在類型參數的限制式中,有不一致的可 Null 性。
  • CS8714 - 類型在泛型型別或方法中不能當做型別參數使用。型別引數的可 NULL 性不符合「非 NULL」條件約束。
  • CS8764 - 傳回型別是否可為 Null 的情況,與覆寫的成員不相符 (可能的原因是屬性可為 Null)。
  • CS8765 - 參數類型是否可為 Null 的情況,與覆寫的成員不相符 (可能的原因是屬性可為 Null)。
  • CS8766 - 傳回型別中參考型別是否可為 Null 的情況,與隱含實作的成員不相符 (可能的原因是屬性可為 Null)。
  • CS8767 - 參數類型中參考型別是否可為 Null 的情況,與隱含實作的成員不相符 (可能的原因是屬性可為 Null)。
  • CS8768 - 傳回型別中參考型別是否可為 Null 的情況,與實作的成員不相符 (可能的原因是屬性可為 Null)。
  • CS8769 - 參數類型中參考型別是否可為 Null 的情況,與實作的成員不相符 (可能的原因是屬性可為 Null)。
  • CS8819 - 傳回型別中參考型別的可 Null 性與部分方法宣告不符。

下列程式碼將示範 CS8764

public class B
{
    public virtual string GetMessage(string id) => string.Empty;
}
public class D : B
{
    public override string? GetMessage(string? id) => default;
}

上述範例顯示基底類別的 virtual 方法和 override 具有不同 Null 性。 基底類別會傳回不可為 Null 的字串,但衍生類別會傳回可為 Null 的字串。 若反轉 stringstring? 則會允許,因為衍生類別的限制更嚴格。 同理可證,參數宣告應相符。 即使基底類別未允許 Null,覆寫方法中的參數也可允許。

其他情況可能會產生這些警告。 介面方法宣告和該方法的實作可能不符。 或者委派類型和該委派的運算式可能不同。 型別參數和型別引數的可 Null 性差異可能不同。

若要修正這些警告,請更新適當的宣告。

程式碼不符合屬性宣告

上述各節已討論如何使用屬性進行可為 Null 的靜態分析,以通知編譯器程式碼的 Null 語意。 若程式碼不符合該屬性的 Promise,編譯器會警告您:

  • CS8607 - 可能的 Null 值不能用於標示 [NotNull][DisallowNull] 的型別
  • CS8763 - 標示 [DoesNotReturn] 的方法不應傳回。
  • CS8770 - 方法缺少 [DoesNotReturn] 註釋,與實作或覆寫的成員不相符。
  • CS8774 - 成員在結束時必須有非 Null 值。
  • CS8775 - 成員在結束時必須有非 Null 值。
  • CS8776 - 成員不可用於此屬性中。
  • CS8777 - 參數在結束時必須有非 Null 值。
  • CS8824 - 參數在結束時必須有非 Null 值,因為參數為非 Null。
  • CS8825 - 因為參數不是 Null,所以傳回值必須非 Null。

請考慮下列 方法:

public bool TryGetMessage(int id, [NotNullWhen(true)] out string? message)
{
    message = null;
    return true;

}

編譯器會產生警告,因為 message 參數被指派為 null,且該方法會傳回 trueNotNullWhen 屬性表示不應發生。

若要解決這些警告,請更新程式碼以符合您所套用屬性的期望。 您可變更屬性或演算法。

詳盡 Switch 運算式

Switch 運算式必須詳盡,表示必須處理所有輸入值。 即使是不可為 Null 的參考型別,也必須考慮 null 值。 編譯器會在未處理 Null 值時發出警告:

  • CS8655 - Switch 運算式不會處理某些 Null 輸入 (並不詳盡)。
  • CS8847 - Switch 運算式不會處理某些 Null 輸入 (並不詳盡)。不過,具有『when』子句的模式可能會成功比對此值。

下列範例程式碼示範此狀況:

int AsScale(string status) =>
    status switch
    {
        "Red" => 0,
        "Yellow" => 5,
        "Green" => 10,
        { } => -1
    };

輸入運算式為 string,而不是 string?。 編譯器仍會產生這個警告。 { } 模式會處理所有非 Null 值,但不符合 null。 若要解決這些錯誤,您可新增明確的 null 案例,或將 { } 取代為 _ (捨棄) 模式。 捨棄模式會比對 Null 及任何其他值。