上一次寫這個主題是兩年前了,當時 Nullable Reference Types 語法仍未定案,而現在已是 C# 8 的新功能之一。先前那篇文章有些內容已經過時,也不夠完整,故整理這篇筆記來更新相關知識。
內容綱要:
正因為參考型別的變數預設可為 null,而且在執行時期隨時都有可能為 null,所以我們以往在寫 C# 程式的時候,常常得在程式各處寫一些安全防護的程式碼:如果某變數不是 null 才繼續做某件事。例如:
就上例來說,我們在寫 StrLen 函式時,並沒有辦法確定傳入的參數 text 究竟有沒有值;如果不先檢查變數是否為 null 就使用它的屬性或方法,那麼當程式執行時,只要呼叫端傳入 null,就會引發
然而,變數為 null 的情形可能到處都是,防不勝防,如果在編譯時期就能盡量避免這類潛在問題,應用程式必然更加穩固,開發人員也能少寫一些重複瑣碎的程式碼,如此不僅減輕了人的負擔,程式碼也更簡潔、更明白呈現程式碼的意圖。這便是 C# 8 加入 Nullable References 的主要原因。
一旦你決定在程式中使用 C# 8 的這項新功能,在宣告參考型別的變數時,若允許它為 null,則必須在型別後面附加一個問號('
你會發現,原本常用的語法(上面範例的第一行),在加入 Nullable Reference Types 功能之後被賦予了新的意義;換言之,以往的參考型別是預設可為 null,現在變成預設不可為 null 了。就語意而言,這是蠻大的改變,而且必然對既有的程式碼帶來不少衝擊,故在預設情況下,Nullable Reference Types 功能是關閉的。
Visual Studio 2019 目前的版本已經沒有提供視覺化介面來修改專案所使用的 C# 版本。按官方文件的解釋,這是為了確保你在程式中使用的 C# 語法皆可相容於專案的 target framework。如果你想要把預設的 C# 版本改為其他版本,仍可以透過修改 .csproj 檔案的方式來達成,作法是在
或
但請注意,官方文件也有提醒:
💬 順便一提,當我把一個舊專案的「目標 framework」從 .NET Core 2.0 改成 .NET Core 3.1 之後,底下這行程式碼無法通過編譯:
錯誤訊息是:
原來問題出在第 6 行的
解決方法很簡單,只要把 .csproj 檔案裡面的
那麼,在使用 C# 8 來編譯程式的情況下,不做任何額外的設定,底下的程式碼能夠通過編譯嗎?
可以通過編譯,但是伴隨警告:
為了讓開發人員能夠更彈性地應付各種狀況,C# 提供了兩種面向的控制開關:
為什麼這兩種開關都以「context」(環境、上下文)來命名呢?我想這大概是因為 C# 可以讓我們針對任意範圍的(甚至只有一行)程式碼來控制與 Nullable References 語法有關的編譯行為。
再重複一次:預設情況下,Nullable References 語法和編譯警告都是關閉的(disabled)。也就是說,即使不修改任何程式碼,你的既有 C# 專案也能跟以往一樣順利通過編譯,而且不會出現與 Nullable References 有關的警告訊息。
接著來看看如何控制這些開關。
在個別檔案中使用
你可以在程式碼的任何地方使用
你也可以只對局部程式碼區塊啟用 Nullable References 語法,作法是在需要啟用新語法的地方加上
上列程式碼可以順利通過編譯,而且沒有任何警告訊息。其中 str1 是可為 null 的字串,而 str2 也是可為 null 的字串;差別只在於前者使用的是 C# 8 的 Nullable Refernce Types 語法,後者則為舊版 C# 語法。如果把這兩行程式碼對調,則會各自引發編譯警告:
編譯警告 CS8600 的內容是:
底下列出
有了這些控制開關,你就可以採取循序漸進的方式來把既有的 C# 專案逐漸修改成 C# 8 的 Nullable References 語法。比如說,先針對少數幾個檔案加入
與
你甚至可以把控制範圍擴及整個方案(solution),作法是在方案的根目錄下建立一個名為 Directory.Build.props 的檔案,內容則和 .csproj 裡面的寫法一樣。參考底下的範例:
Cezary Piątek 甚至提供了一個現成的 EditorConfig 檔案,以便將 Nullable References 相關的編譯警告訊息提升至「錯誤」等級(你需要 Visual Studio 2019 v16.3 或更新的版本)。
Nullable Reference Types 還有一些相關議題並未在本文提及,例如「null 寬容運算子」(null-forgiving operators)、Nullable attributes 等等。也許再找時間寫一篇續集吧。
Happy coding!
- 簡介
- 開啟 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# 版本如下圖(摘自微軟線上文件):<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 的型別。編譯警告 CS8632 的內容是:
Converting null literal or possible null value to non-nullable type.
可為 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
之外,你也可以在這裡使用 disable
、 warnings
、annotations
。
你甚至可以把控制範圍擴及整個方案(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!