初探 C# 8 的 Nullable Reference Types

来源:https://www.cnblogs.com/huanlin/archive/2018/02/18/csharp8-nullable-reference-types.html
-Advertisement-
Play Games

C# 8 打算引入 Nullable Reference Types,這表示往後所有的參考型別預設都是不可為 null;對於可為 null 的參考型別變數,寫法跟可為 null 的實質型別一樣,宣告時必須在型別後面加上 "?" 字元。在這篇筆記中,我試著把我理解的部分整理出來。 ...


溫馨提醒:本文提及的 C# 8 新功能雖已通過提案,但不代表將來 C# 8 正式發布時一定會納入。這表示我這篇筆記有可能白寫了,也表示您不必急著瞭解這項新功能的所有細節,可能只要瞄一下底下的「概要」說明就夠了。

概要

C# 8 的 Nullable Reference Types 意味著往後所有的參考型別預設都是不可為 null;對於可為 null 的參考型別變數,寫法跟可為 null 的實質型別一樣,宣告時必須在型別後面加上 "?" 字元。請看以下範例:

1 int? num = null;         // 可為 null(實質型別)
2 string? userName = null; // 可為 null(參考型別)
3 string password = null;  // 編譯警告: 不可為 null!
4 Employee? emp = null;    // 可為 null(參考型別)
5 User user = null;        // 編譯警告: 不可為 null!

這項新功能有不少細節,不容易幾句話帶過。所以,接下來是比較冗長、也比較詳細的說明。

內容大綱:

  • 基礎知識
  • Nullable Reference Types 是什麼?
  • 為什麼需要 Nullable Reference Types?
  • 不允許 Null 的參考型別
    - default 表示式
  • 允許 Null 的參考型別
    - 靜態路徑分析
  • 安裝工具
    - Sharplab.io
    - 安裝擴充功能:Nullable Reference Types Preview  
  •  編譯既有項目
    - 調整編譯器的警告   

 

基礎知識

一直以來,C# 開發人員已經很熟悉實質型別(value types)和參考型別(reference types)所代表的意義,以及它們之間的差異:

  • 在宣告一個實質型別的變數時(例如 int i),如果沒有賦予初始值,那麼編譯器會將它設定成該型別的預設值。例如 int 型別的預設值是 0。
  • 如果要讓實質型別的變數值可為 null,則要使用泛型結構  System.Nullable<T> ,其中的 T 必須是實質型別。此泛型結構在 C# 中可以用更簡便的 T? 語法來表示,例如 int? 等同於 Nullable<int>,代表可為 null 的整數。
  • 在宣告一個參考型別的變數時(例如 string s),如果沒有賦予初始值,那麼它的預設值便是 null。也就是說,參考型別的變數是可以為 null 的。

 

那麼,既然參考型別本來就是可以是 null,C# 8 (計畫)新增的功能裡面怎麼會有一項叫做「可為空的參考型別」(Nullable Reference Types)呢?它究竟是什麼?

註:如果您想一邊閱讀,一邊把程式碼敲進去實驗看看,可以先跳到後面,找到有關安裝 C# 8 實驗性擴充功能的說明。

 

Nullable Reference Types 是什麼?

簡單地說,在 C# 8 以前,參考型別都是可為 null 的,這點到了 C# 8 將開始改變:所有參考型別預設都是不可為 null;如果想要讓某個參考型別的變數可以為 null,則必須明確宣告它「可以為空」。請看以下範例:

string s1 = null;  // C# 7.x OK,C# 8 出現編譯警告。
string? s2 = null; // C# 7.x 編譯失敗,C# 8 O

 

第 1 行程式碼在 C# 8 會出現下列編譯警告:

Warning CS8600: Cannot convert null to non-nullable reference.

因為 string 是參考型別,而就如剛才提過的,參考型別到了 C# 8 已經改成預設不可為空,故編譯器會發出警告,告訴你不能把 null 指派給「不可為 null 的參考」。

另一方面,第 2 行程式碼在 C# 8 編譯沒問題,若使用 C# 7.2 或更早之前的編譯器,則會出現編譯錯誤:

error CS0453: The type ‘string’ must be a non-nullable value type in order to use it as parameter ‘T’ in the generic type or method ‘Nullable<T>’

這是因為  string?  是 C# 8 的 Nullable Reference Types 語法,表示要宣告一個可為 null 的字串變數。C# 7.2 沒有這種語法,便會出現編譯錯誤。

如您所見,「可為空的參考型別」跟「可為空的實質型別」的語法到了 C# 8 已經統一了:宣告時在型別後面加個問號(’?’)。

為什麼需要 Nullable Reference Types?

在 C# 8 之前,由於參考型別的變數值有兩種可能:null 或者不是 null,編譯器在這裡幫不上忙,所以在處理參考型別的變數時,我們常常會加上一些「防護罩」,也就是預先檢查變數是否為 null,例如:

1 static int Length(string text)
2 {
3     return text == null? 0 : text.Length;
4 }

就上例來說,我們在寫 Length 函式時,根本無法確定傳入的參數 text 究竟有沒有值。如果不先檢查變數是否為 null 就貿然使用它的屬性或方法,那麼當程式執行時,只要呼叫端傳入一個 null,就會發生 NullReferenceException 類型的錯誤。可是,寫程式的時候難免有些地方會忘記加上 null 檢查,防不勝防,這個部分能不能請編譯器幫點忙呢?(C# 8 表示:十分樂意!)

從上面的例子可以看得出來,既有的參考型別在表達 null 語意方面的不足。我們需要更明確的語法來表達下列意圖:

  • 某個參考型別的變數可以是 null,因此,在存取該變數所代表的物件成員時,必須先檢查它是否為 null。 
  • 某個參考型別的變數必定要指向某個物件,而不可以是 null。


在 C# 8 以前,我們已經有了第一項。C# 8 則為了要滿足第二項需求,因而加入 Nullable Reference Types。而且,它希望開發人員在寫程式時要明確指定某個參考型別的變數是可以為 null 的;若不明白指定,則預設是不可為 null。如此一來,編譯器就能在檢查 null 這種細瑣工作上提供協助,讓我們在寫程式的時候就能避免將來程式執行時可能發生的問題。 

C# 8 的這項功能的用意是協助減少執行時期發生 NullReferencException 的機率,而非保證完全杜絕——這裡面有太多「眉角」和漏洞,不容易做到百分百。

 

不允許 Null 的參考型別

以剛才的範例來說,如果想要讓 Length 方法的 text 參數不允許 null,原本的程式碼完全不用改,仍然是原本的樣子:

1 static int Length(string text)
2 {
3     return text == null? 0 : text.Length;
4 }

C# 8 編譯器會認為參數 text 必須有值。因此,當呼叫端傳入 null,例如:

int i = Length(null);

此時編譯器就能檢查得出來,這個呼叫埋藏著 NullReferenceException 的風險,因而顯示編譯警告訊息。請註意只是警告,而非編譯錯誤,這點很重要——這樣才不會因為開發工具(編譯器)升級而導致既有項目突然出現一大堆編譯錯誤。

也因為它只是警告,所以原本已經有加入 null 檢查的防護罩,最好別多事去把它拿掉。像底下這寫法就有自廢武功的疑慮: 

1 static int Length(string text)
2 {
3     return text.Length; // 把預先檢查 null 防護拿掉(不好!)。
4 }

因為,如果開發人員忽略了 C# 8 編譯器的警告:「這裡把 null 指派給不該為 null 的變數了。」那麼程式執行時就可能出現那個惡名昭彰的錯誤。

 

default 表示式

當你對一個不可為 null 的參考型別變數賦予預設值,C# 8 編譯器會出現警告:

string s = default;  // 編譯警告:把 null 指派給不可為 null 的變數

 

允許 Null 的參考型別

延續前面的範例,如果要讓參數 text 接受 null,便需要用到 C# 8 的新語法,也就是在宣告變數的時候,型別的後面加上一個問號。如下所示: 

1 static int Length(string? text) // text 可以傳入 null。
2 {
3     return text == null? 0 : text.Length;
4 }

 

C# 8 編譯器會把上述程式碼編譯成類似底下這樣: 

1 public static int Length([Nullable] string s)
2 {
3     return (s == null) ? 0 : s.Length;
4 }

其中的 [Nullable] 特徵項的全名是  System.Runtime.CompilerServices.NullableAttribute 。

靜態路徑分析

編譯器會分析程式碼的執行路徑,以便抓出可能發生  NullReferenceException  的地方。請看以下範例: 

1 static int Length(string? s)  // 參數 s 可以為 null。
2 {
3     if (s == null)   // 預先檢查 s 是否為 null
4         return 0;    // 是 null 就返回 0。
5     s = null;        // s 又變成 null 了!
6     return s.Length; // 編譯器還是能偵測到,並警告:可能為 null!
7 }

 

可是,程式碼靜態分析無法做到百分之百。應該說,靜態分析只能檢查到方法本身的參數宣告以及區域變數所涉及的程式邏輯,但無法深入到方法中呼叫的其他方法。以剛才的範例來說:

1 static int Length(string? s)
2 {
3     if (String.IsNullOrEmpty(s)) // 編譯警告:CS8604。
4         return 0;
5     return s.Length; // 編譯警告:CS8602。
6 }

這麼安全的寫法,編譯器竟然顯示兩個警告耶!分別是:

  • Warning CS8604: Possible null reference argument for parameter ‘value’ in ‘bool string.IsNullOrEmpty(string value)’. 
  • Warning CS8602: Possible dereference of a null reference. 


出現警告 CS8604 的原因,是我們把可以為 null 的字串變數 s 傳入一個不可為 null 的參數( IsNullOrEmpty  的傳入參數是宣告成  string s ,所以它是不可為 null)。

出現警告 CS8602 的原因,則是剛才提到的,程式碼靜態分析不會再深入下一層方法呼叫,所以也就無法得知  String.IsNullOrEmpty()  的執行結果到底是傳回 true 還是 false。對此問題,C# 提出了一個語法,讓開發人員協助編譯器減少這類因為無法深入分析程式碼而產生的多餘警告。這個語法使用的是驚嘆號(!),像這樣:

1 static int Length(string? s)
2 {
3     if (String.IsNullOrEmpty(s)) // 編譯警告:CS8604。
4         return 0;
5     return s!.Length; // 這行不再引起編譯警告了。
6 }

其中的  s!  就是在告訴編譯器:「嘿!這裡我比你清楚,s 肯定不會是 null,你就別費心警告我了。」

那麼,另一個警告 CS8604 如何解決呢?目前我只知道這兩種方法:

  • 透過編譯選項或前置處理指示 #pragma 來抑制這個警告。
  • 改用  if (s == null) 。


而官方 github 的這份文件:The C# Nullable Reference Types Preview 裡面就有提到  string.IsNullOrEmpty  的問題,上面的回答是:

We are investigating allowing annotations on methods that describe simple relationships between parameters and return value with respect to null values. The compiler could use those annotations when analyzing the calling code.


看來只能等等看後續發展了。

語法的部分就先寫到這裡。接下來是比較輕鬆的部分:工具的安裝與設定。


安裝工具

想要嘗試 C# 8 新增的 Nullable Reference Types 語法,目前我試過兩種方法,一個是用 Visual Studio 2017 加上實驗版本的 Roslyn 擴充功能 Nullable Reference Types Preview;另一個方法則是用瀏覽器開啟 https://sharplab.io,直接線上編譯和測試 C# 8 的新功能。

如果你不想「弄髒」自己目前的開發環境,那麼 sharplab.io 會是個不錯的選擇。當然啦,它跟 Visual Studio 2017 比起來,反應的速度還是比較慢一點。

以下分別說明兩種工具的安裝或設定方式。

Sharplab.io  

用法很簡單,用瀏覽器開啟 https://sharplab.io,左邊窗格可以輸入程式碼,右邊窗格則可以顯示程式的執行結果或者反組譯之後的程式碼,包括 C#、VB、IL、和 JIT 組合語言(酷!)。

如果要實驗 C# 8 的 Nullable Reference Types,必須在左邊窗格上方的下拉選單裡面挑選對應的編譯器版本,如下圖所示:

 

然後就可以敲入程式碼來觀察程式的執行結果、編譯器顯示的警告或錯誤訊息、以及反組譯之後的程式碼。


安裝擴充功能:Nullable Reference Types Preview

前提:你的 Visual Studio 2017 版本必須是 15.5.x,才能安裝此實驗性質的擴充功能。

步驟:

  1. 關閉 Visual Studio 2017。
  2. 下載 Roslyn_Nullable_References_Preview.zip。
  3.  解開剛才下載的壓縮檔,然後執行 install.bat。

下次開啟 Visual Studio 2017,C# 編譯器應該就會認得 Nullable Reference Types 語法了。

註意!安裝此 Roslyn 擴充功能之後,如果後來要安裝 Visual Studio 2017 的其他更新,必須先移除這個實驗性質的擴充功能。移除的方法很簡單,執行 uninstall.bat 即可。


值得一提的是,如果你曾經修改過項目選項中的 C# 版本,最好檢查一下:開啟項目的屬性(Properties)視窗,然後左邊面板選擇 Build,然後在此「建置設定」視窗的最下方找到 Advanced 按鈕。點此按鈕之後,看一下 Language version,如下圖:

 

 

安裝好上述擴充功能後,在我的環境上,C# 語言的版本選擇 latest major version (default)或 latest minor version (latest) 都可以使用 Nullable Reference Types 語法,其他則不行。

這兩個選項可以讓 C# 設計團隊控制哪些新功能要出現在我們使用者的開發環境中。若想嚐鮮,可以選擇 latest minor;若偏好沉穩路線,等新功能正式發布之後才用於項目的話,便可選擇 latest major(適合新項目)。至於想要用更舊的版本來建置項目,就選擇其他特定的 C# 版本,例如 C# 5、6、或 7.2 等等(適合 legacy 項目)。

試試編譯既有項目

按前述步驟安裝好工具之後,用 Visual Studio 2017 開啟我先前寫的一個用來製作點字書的C# 項目,Rebuild 整個 solution,結果建置成功,但有 765 個警告,如下圖:

 

這些警告是:

  • Warning CS8600: Cannot convert null to non-nullable reference. 
  • Warning CS8601: Possible null reference assignment. 
  • Warning CS8602: Possible dereference of a null reference.  
  • Warning CS8618: Non-nullable field 'field-name' is uninitialized.


其中以最後一個 CS8618 的數量最多。

從這裡也可以看得出來,C# 8 推出的這項新功能對既有的 C# 程式碼並不會產生「侵入性」的影響——我的意思是,它並不強迫你改寫或重寫既有的程式碼。然而,每次建置項目都出現這麼多編譯警告,還是會造成困擾。比如說,有些我們特別在意的編譯警告可能因為夾雜在一大堆警告訊息當中而不易發現。

調整編譯器的警告

用新版開發工具(編譯器)來編譯遺留項目時,若要避免產生一大堆編譯警告,最簡單迅速的方法就是把項目屬性中的語言版本設定成比較舊的版本,例如剛才提過的,C# 6、7.0、7.2 等等。當然,這也就等於完全捨棄 C# 8 的新功能了。

如果想要使用某些 C# 8 的新語法,但是又要關閉某些礙眼的警告,則可以在 C# 項目的屬性視窗中設定 Suppress warnings,如下圖:

 

或者也可以把某些警告視為錯誤,即上圖中的 Treat warning as errors 選項。

另外,對於少量的情形,我們還可以在程式中加上前置處理指示 #pragma 來關閉和恢復特定警告,像這樣:

#pragma warning disable CS8618 // Non-nullable field is uninitialized.
  程式碼
#pragma warning restore CS8618

 

重點整理

  • 在 C# 8 以前,參考型別都是可為 null 的,這點到了 C# 8 將開始改變:所有參考型別預設都是不可為 null。
  • 對於可為 null 的參考型別變數,寫法跟可為 null 的實質型別一樣,宣告時必須在型別後面加上 "?",例如 string? text;。這是 C# 8 新增的 Nullable Reference Types 語法。
  • Nullable Reference Types 的用意是協助減少執行時期發生  NullReferenceException  的機率,而非保證完全杜絕。
  • 程式碼靜態分析只能檢查到方法本身的參數宣告以及區域變數所涉及的程式邏輯,但無法深入到方法中呼叫的其他方法。
  • 對於既有的 C# 項目,我們可以在不修改程式碼的前提下繼續使用新版的編譯器,並透過編譯選項來避免編譯時產生一堆警告訊息。

 

寫完這篇筆記,有一個感覺:C# 8 打算引入 Nullable Reference Types  來減少 NullReferenceException 的問題,卻也帶來了一些問題。

那麼,C# 8 正式發布的那一天,真的會包含 Nullable Reference Types 特性嗎?

 

參考資料
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • HQL查詢:hibernate獨有的查詢語言 適用於不複雜的多表查詢 示例: 實體類: package domain; public class Customer { private Long cust_id; private String cust_name; private String cus ...
  • 一、flask a、Flask是一個基於Python開發並且依賴jinja2模板和Werkzeug WSGI服務的一個微型框架,對於Werkzeug本質是Socket服務端,其用於接收http請求並對請求進行預處理,然後觸發Flask框架,開發人員基於Flask框架提供的功能對請求進行相應的處理,並 ...
  • 題目鏈接:http://poj.org/problem?id=2505 題目大意: 兩個人輪流玩游戲,Stan先手,數字 p從1開始,Stan乘以一個2-9的數,然後Ollie再乘以一個2-9的數,直到誰先將p乘到p>=n時那個人就贏了,而且輪到某人時,某人必須乘以2-9的一個數。 解題思路: 這是 ...
  • 原理 選定填充圖片的ASCII字元,不同的字元對應於不同的灰度 讀取圖片並計算各像素灰度值(同時考慮透明背景),用相應的的ASCII字元替換該像素 程式功能 支持3種文件選擇方式:選定文件(支持圖片預覽),添加文件夾,拖入文件 支持5種圖片格式:.jpg, .jpeg, .gif, .png,.bm ...
  • 之前在使用Altera的三速乙太網MAC IP的基礎上,完成了UDP協議數據傳輸。此次為了將設計移植到xilinx FPGA上,需要用到xilinx的三速乙太網MAC IP核,當然也可以自己用HDL編寫,但必須對數據鏈路層協議有非常清晰的認識。以下是在使用xilinx 三速乙太網MAC過程中的一些記 ...
  • 線程(下) 7.同步鎖 這個例子很經典,實話說,這個例子我是直接照搬前輩的,並不是原創,不過真的也很有意思,請看: 這段代碼的意思是,用一百個線程去減1,以此讓變數number為100的變為0 結果: 那麼我稍微的改下代碼看看: 並沒有很大的改變對吧,只是加了一個臨時變數,並且中途停頓了0.2s而已 ...
  • 在控制器方法的頭部添加 [ValidateInput(false)] 如果向mvc服務端提交帶html標簽的內容就會導致校驗失敗異常,從而得不到想要的結果,關閉的方法是在相應方法頭部添加 [ValidateInput(false)]屬性。 如: ...
  • 本文是利用ZXing.Net在WinForm中生成條形碼,二維碼的小例子,僅供學習分享使用。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...