C# 8 的 Nullable Reference Types

上一次寫這個主題是兩年前了,當時 Nullable Reference Types 語法仍未定案,而現在已是 C# 8 的新功能之一。先前那篇文章有些內容已經過時,也不夠完整,故整理這篇筆記來更新相關知識。



內容綱要:
  • 簡介
  • 開啟 Nullable Reference Types 功能
    - 在個別檔案中使用 #nullable 指示詞
    - 專案(project)與方案(solution)層級的 Nullable 設定
  • 編譯器對 Nullable Reference Types 語法的警告訊息列表
  • 重點整理

簡介

Nullable Reference Types 是 C# 8 新增的功能。對於已經用 C# 寫過一些程式的人來說,初次聽到 Nullable Reference 可能會覺得奇怪:宣告為參考型別(reference types)的變數不是本來就可以為 null 嗎?而且如果沒有給值,其預設值就是 null(未指向任何物件)。為什麼還要特別強調「可為 null 的參考」呢?
💬 有時候,我會交替使用「Nullable References」或更簡短的「nullable」來代表 Nullable Reference Types。 

正因為參考型別的變數預設可為 null,而且在執行時期隨時都有可能為 null,所以我們以往在寫 C# 程式的時候,常常得在程式各處寫一些安全防護的程式碼:如果某變數不是 null 才繼續做某件事。例如:

static int StrLen(string text)
{
    return text == null? 0 : text.Length;
}

就上例來說,我們在寫 StrLen 函式時,並沒有辦法確定傳入的參數 text 究竟有沒有值;如果不先檢查變數是否為 null 就使用它的屬性或方法,那麼當程式執行時,只要呼叫端傳入 null,就會引發 NullReferenceException 類型的錯誤。

然而,變數為 null 的情形可能到處都是,防不勝防,如果在編譯時期就能盡量避免這類潛在問題,應用程式必然更加穩固,開發人員也能少寫一些重複瑣碎的程式碼,如此不僅減輕了人的負擔,程式碼也更簡潔、更明白呈現程式碼的意圖。這便是 C# 8 加入 Nullable References 的主要原因。

一旦你決定在程式中使用 C# 8 的這項新功能,在宣告參考型別的變數時,若允許它為 null,則必須在型別後面附加一個問號('?')。請看底下這個簡單的範例:

string str1 = "hello"; // str1 是不可為 null 的字串
string? str2 = null;   // str2 是可為 null 的字串

你會發現,原本常用的語法(上面範例的第一行),在加入 Nullable Reference Types 功能之後被賦予了新的意義;換言之,以往的參考型別是預設可為 null,現在變成預設不可為 null 了。就語意而言,這是蠻大的改變,而且必然對既有的程式碼帶來不少衝擊,故在預設情況下,Nullable Reference Types 功能是關閉的。

開啟 Nullable Reference Types 功能

如果你的應用程式專案的 target framework 是 .NET Core 3.x 以上的版本,便可在專案中使用 C# 8 的新語法。各 framework 所對應的 C# 版本如下圖(摘自微軟線上文件):


Visual Studio 2019 目前的版本已經沒有提供視覺化介面來修改專案所使用的 C# 版本。按官方文件的解釋,這是為了確保你在程式中使用的 C# 語法皆可相容於專案的 target framework。如果你想要把預設的 C# 版本改為其他版本,仍可以透過修改  .csproj 檔案的方式來達成,作法是在 <PropertyGroup> 元素裡面加入一個 <LangVersion> 元素,例如:

<LangVersion>8.0</LangVersion>



<LangVersion>Latest</LangVersion>

但請注意,官方文件也有提醒:
Choosing a language version newer than the default can cause hard to diagnose compile-time and runtime errors.
意思是說,當你要手動調整 C# 版本時,最好是降低版本,而不應超過預設的 C# 版號,以免在開發與除錯的過程中碰到一些奇怪的問題。


💬 順便一提,當我把一個舊專案的「目標 framework」從 .NET Core 2.0 改成 .NET Core 3.1 之後,底下這行程式碼無法通過編譯:

string? str2 = null;  // str2 是可為 null 的字串

錯誤訊息是:
Feature 'nullable reference types' is not available in C# 7.3. Please use language version 8.0 or greater.
把專案原始碼打開來看看:


原來問題出在第 6 行的 <LangVersion> 元素。想必是原先「target framework」還是 .NET Core 2.0 的時候,Visual Studio 便已經把當時使用的 C# 版本紀錄在 .csproj 檔案裡。後來雖然把 target framework 改為 .NET Core 3.1,但原先的 <LangVersion> 元素並沒有跟著調整或移除。

解決方法很簡單,只要把 .csproj 檔案裡面的 <LangVersion> 元素刪除就行了(如此便會由編譯器根據 target framework 來決定預設的 C# 版本)。



那麼,在使用 C# 8 來編譯程式的情況下,不做任何額外的設定,底下的程式碼能夠通過編譯嗎?

string? str2 = null;  // str2 是可為 null 的字串

可以通過編譯,但是伴隨警告:
CS8632: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
如前面提過的,由於 C# 8 的 Nullable Reference Types 是重大改變,對開發人員和既有程式碼都會產生不少衝擊,所以此功能預設是關閉的。當我們想要使用「可為 null 的參考型別」時,就必須明白指示編譯器要開啟這項功能。

為了讓開發人員能夠更彈性地應付各種狀況,C# 提供了兩種面向的控制開關:
  • nullable annotation context:是否啟用 Nullable References 語法。(註:微軟文件裡使用「注釋」,而我選擇把 annotation 稱作「語法」,可能失去一些精確性,但我覺得更好理解。)
  • nullable warning context:是否啟用 Nullable References 相關的編譯警告

為什麼這兩種開關都以「context」(環境、上下文)來命名呢?我想這大概是因為 C# 可以讓我們針對任意範圍的(甚至只有一行)程式碼來控制與 Nullable References 語法有關的編譯行為。

再重複一次:預設情況下,Nullable References 語法編譯警告都是關閉的(disabled)。也就是說,即使不修改任何程式碼,你的既有 C# 專案也能跟以往一樣順利通過編譯,而且不會出現與 Nullable References 有關的警告訊息。

接著來看看如何控制這些開關。

在個別檔案中使用 #nullable 指示詞

你可以在程式碼的任何地方使用 #nullable 指示詞來啟用或關閉 nullable 語法或警告(即剛才提過的 annotation context 和 warning context)。例如,在一個 C# 程式檔案的最上方或者 using 陳述式下方加入一行 #nullable enable,這表示整個檔案裏面的程式碼都會使用 Nullable References 語法。

你也可以只對局部程式碼區塊啟用 Nullable References 語法,作法是在需要啟用新語法的地方加上 #nullable enable,然後在不需要此語法的地方加上 #nullable disable,或者使用 #nullable restore 來回復至專案層級的 nullable 設定(稍後會介紹)。參考以下範例:


上列程式碼可以順利通過編譯,而且沒有任何警告訊息。其中 str1 是可為 null 的字串,而 str2 也是可為 null 的字串;差別只在於前者使用的是 C# 8 的 Nullable Refernce Types 語法,後者則為舊版 C# 語法。如果把這兩行程式碼對調,則會各自引發編譯警告:

編譯警告 CS8600 的內容是:
正在將 Null 常值或可能的 Null 值轉換為不可為 Null 的型別。
Converting null literal or possible null value to non-nullable type.
編譯警告 CS8632 的內容是:
可為 Null 的參考型別註釋應只用於 '#nullable' 註釋內容中的程式碼。
The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
經過前面的說明,相信你應該已經能夠理解為什麼那兩行程式碼會出現編譯警告了。

底下列出 #nullable 指示詞的各種控制組合:
  • #nullable disable:關閉 nullable 語法和編譯警告。(這是預設情形)
  • #nullable enable:開啟 nullable 語法和編譯警告。
  • #nullable restore:從這裡開始套用專案層級的 nullable 設定。
  • #nullable disable annotations:關閉 nullable 語法。
  • #nullable enable annotations:開啟 nullable 語法。
  • #nullable restore annotations:從這裡開始套用專案層級的 nullable 語法開關。
  • #nullable disable warnings:關閉 nullable 相關的編譯警告。
  • #nullable enable warnings:開啟 nullable 相關的編譯警告。
  • #nullable restore warnings:從這裡開始套用專案層級的 nullable 編譯警告開關。

有了這些控制開關,你就可以採取循序漸進的方式來把既有的 C# 專案逐漸修改成 C# 8 的 Nullable References 語法。比如說,先針對少數幾個檔案加入 #nullable enable,感受一下啟用新語法之後,要花多少工夫來修改程式碼,才能消除所有的編譯警告。等到熟練了,覺得更有把握了,再把修改範圍擴及更多 C# 程式檔案。

專案與方案層級的 Nullable 設定

除了檔案層級的  #nullable 指示詞,你也可以在 .csproj 檔案裡面加入 <Nullable>enable</Nullable> 來讓整個專案都啟用 Nullable References 語法。如下所示(第 6 行):


#nullable 指示詞類似,<Nullable> 元素除了指定為 enable 之外,你也可以在這裡使用 disablewarningsannotations

你甚至可以把控制範圍擴及整個方案(solution),作法是在方案的根目錄下建立一個名為 Directory.Build.props 的檔案,內容則和 .csproj 裡面的寫法一樣。參考底下的範例:


編譯器對 Nullable References 語法的警告訊息列表

最後,如果你想要知道 C# 編譯器在檢查 Nullable References 語法時的規則與警告訊息,可以參考這個頁面:CsharpNullableTypeRules.md。這是利用 Cezary Piątek 提供的程式碼所產生的結果,我只是把程式裡面的 "en-US" 改為 "zh-TW" 而已。

Cezary Piątek 甚至提供了一個現成的 EditorConfig 檔案,以便將 Nullable References 相關的編譯警告訊息提升至「錯誤」等級(你需要 Visual Studio 2019 v16.3 或更新的版本)。

重點整理

  • 目標 framework 為 .NET Core 3.x 的專案預設使用的 C# 版本是 8.0。
  • Nullable References 語法對既有程式會產生不小衝擊,故此功能預設為關閉。
  • C# 提供了兩種面向的控制開關:(1)nullable annotation context:是否啟用 Nullable References 語法;以及(2)nullable warning context:是否啟用 Nullable References 相關的編譯警告
  • 我們可以在 .csproj 裡面使用  <Nullable>enable</Nullable> 來進行專案層級的設定,也可以在單一檔案裡面透過編譯指示詞 #nullable enable 來啟用此功能,或者用 #nullable disable 將它關閉,又或者使用 #nullable restore 回復至專案層級的設定。在 solution 根目錄下的 Directory.Build.props 檔案中的設定則可以套用至整個 solution 的全部專案。

Nullable Reference Types 還有一些相關議題並未在本文提及,例如「null 寬容運算子」(null-forgiving operators)、Nullable attributes 等等。也許再找時間寫一篇續集吧。

Happy coding!

參考資料

技術提供:Blogger.
回頂端⬆️