上一次寫這個主題是兩年前了,當時 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.把專案原始碼打開來看看:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>netcoreapp3.1</TargetFramework> | |
<LangVersion>7.3</LangVersion> | |
</PropertyGroup> | |
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> | |
<WarningLevel>3</WarningLevel> | |
</PropertyGroup> | |
</Project> |
原來問題出在第 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 設定(稍後會介紹)。參考以下範例:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#nullable enable // 啟用 nullable 語法和警告 | |
string? str1 = null; | |
#nullable disable // 關閉 nullable 語法和警告 | |
string str2 = null; |
上列程式碼可以順利通過編譯,而且沒有任何警告訊息。其中 str1 是可為 null 的字串,而 str2 也是可為 null 的字串;差別只在於前者使用的是 C# 8 的 Nullable Refernce Types 語法,後者則為舊版 C# 語法。如果把這兩行程式碼對調,則會各自引發編譯警告:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#nullable enable | |
string str1 = null; // 編譯警告 CS8600 | |
#nullable disable | |
string? str2 = null; // 編譯警告 CS8632 |
正在將 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 行):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>netcoreapp3.1</TargetFramework> | |
<Nullable>enable</Nullable> | |
</PropertyGroup> | |
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> | |
<WarningLevel>3</WarningLevel> | |
</PropertyGroup> | |
</Project> |
與
#nullable
指示詞類似,<Nullable>
元素除了指定為 enable
之外,你也可以在這裡使用 disable
、 warnings
、annotations
。
你甚至可以把控制範圍擴及整個方案(solution),作法是在方案的根目錄下建立一個名為 Directory.Build.props 的檔案,內容則和 .csproj 裡面的寫法一樣。參考底下的範例:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project> | |
<PropertyGroup> | |
<Nullable>enable</Nullable> | |
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild> | |
<RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis> | |
</PropertyGroup> | |
</Project> |
編譯器對 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!