可為 Null 的參考型別
在可為 Null 的遺忘式內容中,所有參考型別都是可為 Null。 可為 Null 參考型別是指在可為 Null 感知內容中啟用的功能群組,可將程式碼造成執行階段擲回 System.NullReferenceException 的可能性降到最低。 可為 Null 參考型別包含三個可協助您避免這些例外狀況的功能,包括將參考型別明確標示為 可為 Null 功能:
- 改善的靜態流程分析可判斷變數在反參考之前是否可能為
null
。 - 標註 API 的屬性,讓流程分析可以判斷 Null 狀態。
- 變數註釋,開發人員用來明確宣告變數預期的 Null 狀態。
編譯器會在編譯時期追蹤您程式碼中每個運算式的 Null 狀態。 Null 狀態會是以下三個值之一:
- 「非 Null」: 已知運算式為非
null
。 - 「可能為 Null」: 運算式可能是
null
。 - 「遺忘式」: 編譯器無法判斷運算式的 Null 狀態。
變數註釋會決定參考型別變數的 可 Null 性:
- 「不可為 Null」: 如果您將
null
值或 「可能為 Null」 運算式指派給變數,編譯程式會發出警告。 「不可為 Null」 的變數預設 Null 狀態為「不可為 Null」。 - 「可為 Null」: 您可以將
null
值或 「可能為 Null」 運算式指派給變數。 當變數的 Nll 狀態是 「可能為 Null」 時,如果您反參考變數,編譯器就會發出警告。 變數的預設 Null 狀態為 「可能為 Null」。 - 「遺忘式」: 您可以將
null
值或 「可能為 Null」 運算式指派給變數。 當您反參考變數,或將 「可能為 Null」運算式指派給變數時,編譯器不會發出警告。
遺忘式 Null 狀態和 遺忘式 可 NULL 性符合可 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」。
- 套用至 API 的屬性 為編譯器的 Null 狀態分析提供更多內容。
- 可為 Null 的變數註釋 會提供變數意圖的相關資訊。 註釋對於在成員方法開頭設定的預設 Null 狀態的欄位非常有用。
- 管理 泛型型別引數 的規則。 新增了新的條件約束,因為型別參數可以是參考型別或實值型別。
?
字尾會針對可為 Null 的實值型別和可為 Null 的參考型別以不同的方式實作。 - 可為 Null 的內容 可協助您移轉大型專案。 您可以在移轉時,在應用程式的一部分啟用可為 Null 的內容或警告。 解決更多警告之後,您可以針對整個專案啟用可為 Null 參考型別。
最後,您會了解型別與陣列中 struct
Null 狀態分析的已知陷阱。
您也可以從 C# 中可 null 安全性的 Learn 課程模組,探索這些概念。
Null 狀態分析
啟用可為 Null 的參考型別時,Null 狀態分析 會追蹤參考的 Null 狀態。 變數為 「非 Null」 或 「可能為 Null」。 編譯器會以兩種方式判斷變數是否為不是 Null:
- 將一個已知為「非 Null」 的值指派給變數。
- 檢查變數是否有
null
,而且自該檢查之後未曾修改。
未啟用可為 Null 參考型別時,所有運算式都會有 遺忘式 Null 狀態。 本節的其餘部分會描述啟用可為 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.Parent
之前,以及將 current
傳遞至 ProcessNode
動作之前,會檢查變數 current
的 null
。 先前的範例示範編譯器在初始化、指派或比較 null
時,如何判斷區域變數的 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";
}
}
注意
C# 10 新增了一些明確指派和 Null 狀態分析的改善功能。 當您升級至 C# 10 時,可能會發現誤判為真的可為 Null 警告較少。 若要進一步了解這些改善功能,請參閱改善明確指派的功能規格。
可為 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;
根據檢查,任何開發人員都會考慮此程式碼安全,且不應產生警告。 不過由於 message
為 「可能為 Null」 變數,編譯器並不知道 IsNull
會提供 Null 檢查並對 message.ToUpper()
陳述式發出警告。 使用 NotNullWhen
屬性來修正此警告:
bool IsNull([NotNullWhen(false)] string? s) => s == null;
這個屬性會通知編譯器,如果 IsNull
傳回 false
,則參數 s
不是 Null。 編譯器會將 message
的 Null 狀態變更為 if (!IsNull(message)) {...}
區塊內的 「非 Null」。 不會發出任何警告。
屬性會提供關於引數、傳回值,以及用來叫用成員的物件執行個體成員的 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 的註釋可以變更 Entity Framework Core 判斷是否需要資料成員的方式。 如需詳細資訊,請參閱 Entity Framework Core 基本概念:使用可為 Null 的參考型別一文。
泛型
泛型需要詳細的規則來處理任何型別參數 T
中的 T?
。 因為記錄和可為 Null 實值型別和可為 Null 參考型別的不同實作方式,有必要詳細說明規則。 可為 null 實值型別的實作是使用 System.Nullable<T> 結構。 可為 null 參考型別是當成向編譯器提供語意規則的型別註釋來實作。
- 如果
T
的型別引數是參考型別,則T?
會參考對應的可為 Null 參考型別。 例如,如果T
是string
,則T?
為string?
。 - 如果
T
dr11型別引數是實值型別,則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 的參考型別,例如T
中的string?
,編譯器會產生警告。class?
條件約束表示T
必須是參考型別,不論是不可為 Null (string
) 還是可為 Null 的參考型別 (例如string?
)。 當型別參數是可為 Null 的參考型別 (例如string?
) 時,T?
的運算式會參考該相同的可為 null 參考型別,例如string?
。notnull
條件約束表示T
必須是不可為 Null 的參考型別或不可為 Null 的實值型別。 如果您在型別參數中使用可為 Null 的參考型別或可為 Null 的實值型別,編譯器會產生警告。 此外,當T
是實值型別時,傳回值就是該實值型別,而不是對應的可為 Null 實值型別。
這些限制式有助於將如何使用 T
的詳細資訊提供給編譯器。 這可協助開發人員選擇 T
的型別,並在使用泛型型別的執行個體時提供更好的 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 的參考型別。 - 您可以使用 null 容許運算子
!
,但沒有任何作用。
專案的可為 Null 註釋內容和可為 Null 警告內容都可在您的 .csproj 檔案中使用 <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
:將可為 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
:將註釋警告內容還原至專案設定。
針對任何程式碼,您可以設定下列任何組合:
警告內容 | 註釋內容 | 使用 |
---|---|---|
專案預設值 | 專案預設值 | 預設 |
enable | disable | 修正分析警告 |
enable | 專案預設值 | 修正分析警告 |
專案預設值 | enable | 新增型別註釋 |
enable | enable | 程式碼已移轉 |
disable | enable | 修正警告之前標註程式碼 |
disable | disable | 將舊版程式碼新增至移轉的專案 |
專案預設值 | disable | 很少 |
disable | 專案預設值 | 很少 |
這九種組合可讓您更精細地控制編譯器因程式碼而發出的診斷。 您可以在您要更新的任何區域中啟用更多功能,而不會看到尚未準備好要解決的其他警告。
重要
內容全域可為 null 不適用於產生的程式碼檔案。 不論採用任何一個策略,所有標記為產生的來源檔案,都會「停用」內容可為 null。 這表示產生的檔案中的所有 API,都不會有標註。 有四種方式可將檔案標記為是產生的檔案:
- 在 .editorconfig 中,於套用至該檔案的區段中,指定
generated_code = true
。 - 將
<auto-generated>
或<auto-generated/>
置於檔案頂端的註解中。 它可以在註解的任一行,但註解區塊必須是檔案中的第一個元素。 - 使用 TemporaryGeneratedFile_ 做為檔案名稱的開頭
- 使用 .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 參考型別 FirstName
和 LastName
是 null 時,PrintStudent(default)
沒有任何警告。
另一個較常見的案例是處理泛型結構時。 請考慮下列範例:
#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
會反參考,造成執行階段例外狀況。