重訪 C# 空值安全(上):更方便優雅的語法

之前寫過同樣主題的筆記,這次涵蓋 C# 14,更完整,也添加更多細節。分為上下兩集,這是上集。


C# 從最初的版本開始,就不斷演進其空值處理機制。從 C# 2.0 的 Nullable Value Types (T?),到各種 null 運算子(???.),再到 C# 8 的 Nullable Reference Types,每一步都是為了讓「空值」這件事變得更明確、可追蹤、可由編譯器檢查


本文要介紹現代 C# 的空值安全機制(null safety),分為上下兩集,涵蓋 C# 14。

這是上集,從基礎觀念與語法糖談起。

為什麼需要空值安全?

在傳統 C# 中,參考型別的變數預設可以是 null

string name = null;  // 完全合法
int length = name.Length;  //  運行時爆炸:NullReferenceException

這個問題有多嚴重?NullReferenceException 長期以來都是 .NET 應用程式中最常見的崩潰原因之一。它不僅成本高昂,而且隱蔽性強,往往要等到執行時才爆發。

可為 Null 的實值型別


C# 2.0 引入了可為 Null 的實值型別(Nullable Value Types),也就是 System.Nullable<T> 泛型,讓 intboolDateTime 等實值型別也能表達「無值」的狀態。

語法:簡潔的 T?

雖然你可以寫 Nullable<int>,但 C# 提供了更簡潔的語法糖 T?,這也是目前推薦的寫法:

Nullable<int> x;   // 完整寫法,太囉嗦
int? y;            // 簡潔寫法,推薦!


賦值與取值

int? age = null;      // 可以是 null
age = 25;             // 也可以有值

// 取值方式 1:直接使用
if (age == 25) { ... }

// 取值方式 2:透過 Value 屬性
if (age.Value == 25) { ... }  // 注意:若 age 為 null 會拋出異常!

這裡要特別小心:使用 .Value 前,務必先檢查是否為 null(透過 .HasValue 屬性或 != null),否則會拋出 InvalidOperationException

底層結構:Nullable<T>

Nullable<T> 其實是一個簡單的 struct:

public struct Nullable<T> where T : struct
{
    private bool hasValue;  // 是否有值
    private T value;        // 儲存的實際值
    // ...
}

這意味著 int? 實際上佔用額外的記憶體來儲存 bool 旗標,且檢查 HasValue 是非常高效的。


寫出優雅的 Null 處理邏輯

了解了基礎的 T? 之後,接下來的問題就是如何「處理」它。

如果你發現自己的程式碼充斥著 if (x != null) 的檢查,那麼 C# 提供了一系列強大的運算子,能讓你把這些冗長的檢查簡化為優雅的單行程式碼。

Null 聯合運算子(??

Null 聯合運算子(coalescing operator)是最常用的運算子之一,寫法為兩個問號 ?? 。它的語意很直白:「如果左邊不是 null,就用左邊;否則用右邊。」


範例:

// 提供預設值
string name = GetUserName() ?? "訪客";

這行程式碼等同於:

var temp = GetUserName();
string name = (temp != null) ? temp : "訪客";

Null 聯合指派運算子(??=

Null 聯合指派運算子(coalescing assignment operator)是 C# 8 引入的語法糖,專門處理「如果變數是 null,就給它賦值」的情境,常用於延遲初始化

範例:

private List<string>? _cache;

public List<string> GetCache()
{
    // 如果 _cache 是 null,就載入資料並指派給它;否則直接回傳
    _cache ??= LoadFromDatabase();
    return _cache;
}

Null 條件運算子(?.

Null 條件運算子(conditional operator)大概是讓 C# 開發者最「有感」的改進(又稱貓王運算子)。有了它,我們可以優雅地處理深層物件存取:

// 只要 customer 或 Order 為 null,結果就是 null
int? orderId = customer?.Order?.Id;

只要鏈條中任何一個環節是 null,整個運算式就會立即短路(short-circuit)並回傳 null,而不會拋出異常。


搭配索引子(?[]

string firstItem = list?[0];  // 如果 list 為 null,結果就是 null

Null 條件指派(C# 14 新功能)

C# 14 進一步擴展了條件運算子 ?. 的能力,讓它可以放在指派運算子的左邊,稱為 Null 條件指派(conditional assignment)。

範例:

// 只有當 customer 不是 null 時,才執行後面的指派動作
customer?.Order = newOrder;

組合技:現代 C# 的建議寫法

我們可以把上述運算子組合起來,寫出既安全又具備預設值的強大邏輯:

// 若 user 為 null,或 Name 為 null,則回傳 "匿名"
string displayName = user?.Name ?? "匿名";

// 讀取設定,如果中間任何物件為 null,就使用預設值 128
int bufferSize = config?.Settings?.BufferSize ?? 128;

下回預告

有了這些強大的運算子,處理 Null 確實方便多了。但真正的治本之道,是讓編譯器幫我們預防 Null 的產生。

下一篇文章將深入探討 C# 8.0 最具革命性的功能——Nullable Reference Types (NRT),看看如何讓編譯器成為你的空值安全守門員。

Keep learning!

相關文章

沒有留言:

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