快速了解C# 8.0中「可空引用類型(Nullable reference type)」語言特性

Visual C# 8.0中引入了可空引用類型(Nullable reference type),通過編譯器提供的強大功能,幫助開發人員儘可能地規避由空引用帶來的程式碼問題。這裡我大致介紹一下可空引用類型的基本內容。
剛開始接觸這個語言特性的時候,可能會不太容易理解。引用類型本來不就是可以為空(null)的么,為啥還要特別地引入「可空引用類型」的概念呢?其實這是從編譯器的角度要求開發人員在編程的時候就考慮某個變數是否有可能為空,從而儘可能地減少由空引用所帶來的程式碼錯誤。
假設有如下類:

class Student
{
    public Student(string name, DateTime dayOfBirth)
        => (Name, DayOfBirth) = (name, dayOfBirth);

    public string Name { get; set; }

    public DateTime DayOfBirth { get; set; }

    public string Notes { get; set; }
}

此類定義了一個「學生」實體的基本資訊,為了簡化起見,這裡只列出了需要討論的幾個屬性:

  • Name:學生姓名
  • DayOfBirth:學生生日
  • Notes:對學生資訊的一些備註

假設我們有兩個操作:在所有學生中,找出所有具有備註資訊的學生,以及對所有學生按姓名排序,在C#中很容易使用Linq來實現:

var studentsHasNotes = students.Where(s => s.Notes.Length > 0);

以及:

var orderedStudents = students.OrderBy(s => s.Name);

到目前為止沒啥問題,程式能夠正常運行。然而仔細進行程式碼審查不難發現,在獲取所有具有備註資訊的學生的程式碼中(也就是上面第一段程式碼中),有可能出現空引用的異常,因為對於一個「學生」實體來說,它的Notes屬性是有可能為null的。
現在我們打開「可空引用類型」這一語言特性,打開方式主要有兩種:可以在項目級別,編輯csproj項目文件進行設置,也可以通過#nullable預編譯指令來實現:

  • 編輯csproj項目文件,加入<Nullable>enable</Nullable>即可:
    image
  • 通過#nullable預編譯指令來實現,只需要在程式碼中需要的地方加入#nullable指令即可:
    image

啟用「可空引用類型」這一語言特性之後你會發現,在上面的Student類的構造函數處出現了一個警告,提示在構造函數執行完成時,不可為空的「Notes」屬性需要有一個不為空的值,建議將其設置為可空的string類型。為什麼編譯器僅提示Notes有可能為空,而不是Name屬性呢?因為構造函數中已經為Name賦值了,因此,對於任何一個Student的對象,Name不可能為空,而Notes則不然。

image

Name不可能為空?它不是string類型么?萬一在程式碼中它為空了怎麼辦?別急,編譯器是不會允許出現這種情況的:

image

在此,我們將Notes屬性設置為string?類型,於是你會發現,位於構造函數上的警告資訊已經沒有了,因為我們允許Student對象可以沒有Notes數據,但在「找出所有具有備註資訊的學生」這一操作時,又會出現警告,提示說Notes有可能為空:

image

於是,你會發現,在啟用了可空引用類型的語言特性後,我們就需要仔細考察Student類型中的每一個引用類型的屬性,看它在實際應用中是否有可能為空,如果可能為空,則用可空引用類型來定義屬性,之後編譯器就會幫助你來分析哪些地方有可能存在空引用。

在上面的「找出所有具有備註資訊的學生」例子中,如果你覺得Notes肯定不會為空,那麼也可以使用「!」操作符來覆蓋編譯器的警告資訊,比如:

image

現在流行的.NET開源框架基本上都已經支援了可空引用類型了,而且如果你是一名開源框架的開發人員,也強烈建議在你的框架中啟用這一語言特性來儘可能地避免空引用問題。比如,如果你在程式碼中啟用了可空引用類型特性,那麼當你從Newtonsoft.Json的JsonConverter類繼承時,你會發現,你必須使用可空引用類型的函數重載:

image

但如果你沒有啟用可空引用類型特性,那麼當你從Newtonsoft.Json的JsonConverter類繼承時,你會發現,重載函數的簽名與以前一樣:

image

好了,對於C# 8.0的「可空引用類型」大致就介紹這麼多,相信應該已經基本上概括了它的要點和使用方式,在日常開發中應該夠用了。