前言
空值判斷 是 C# 開發中最基礎也最容易出錯的環節。null、""、string.Empty、DBNull.Value、Nullable<T> 等概念混淆,常導致 NullReferenceException、資料庫存取錯誤或邏輯缺陷。本文系統整理各類空值表示、判斷方式、效能比較與最佳實務。
四大空值類型對照表
| 類型 | 適用對象 | 記憶體行為 | 常見場景 |
null | 參考型別、可為 Null 的實值型別 | 參考指向「無物件」 | 物件未初始化、資料庫 NULL 欄位 |
"" / string.Empty | 字串型別 | 指向長度為 0 的字串物件 | 使用者未輸入、選擇性欄位 |
DBNull.Value | ADO.NET 資料庫欄位 | 單一靜態實例,代表「資料庫 NULL」 | DataReader、DataRow 欄位值 |
Nullable<T> (T?) | 實值型別 (int, bool, DateTime...) | 結構體包裝,HasValue=false 表無值 | 資料庫對應、選擇性參數 |
字串空值判斷:三種寫法效能比較
string s = GetString();
// 效能由高到低:
// 1. 長度檢查 (最快,避免建立比較字串)
if (s.Length == 0) { ... }
// 2. 靜態 Empty 比較 (編譯器優化,極接近 1)
if (s == string.Empty) { ... }
// 3. 常值比較 (會建立字串常值,極微小差異)
if (s == "") { ... }
// ⚠️ 危險:未檢查 null 直接存取 Length 會拋出 NullReferenceException
// 安全寫法:
if (s != null && s.Length == 0) { ... }
if (!string.IsNullOrEmpty(s)) { ... } // .NET 2.0+ 推薦
if (!string.IsNullOrWhiteSpace(s)) { ... } // .NET 4.0+ 推薦 (含空白字元)
效能測試參考 (1 億次迴循)
| 方法 | 相對耗時 | 備註 |
| s.Length == 0 | 1.00x (基準) | 最快,但需先確認非 null |
| s == string.Empty | 1.02x | 編譯器優化為相同 IL |
| s == "" | 1.03x | 極微小差異,可忽略 |
| string.IsNullOrEmpty(s) | 1.15x | 含 null 檢查,最安全實用 |
null vs "" vs string.Empty 關鍵差異
// 記憶體圖示:
string strNull = null; // 參考指向 無 (0x00000000)
string strEmpty = ""; // 參考指向 字串物件 (長度=0)
string strEmpty2 = string.Empty; // 同一個靜態空字串物件 (字串駐留)
// 行為差異:
strNull.Length; // ❌ NullReferenceException
strEmpty.Length; // ✅ 0
strNull.ToString(); // ❌ NullReferenceException
strEmpty.ToString(); // ✅ ""
// 判斷邏輯:
strNull == ""; // false
strNull == string.Empty; // false
string.IsNullOrEmpty(strNull); // true
DBNull 深度解析:資料庫 NULL 的特殊表示
為什麼需要 DBNull?
.NET 的實值型別 (int, DateTime, bool...) 無法直接表示「無值」。ADO.NET 使用 object 儲存欄位值,引入 DBNull 類別 (單例模式) 專門代表「資料庫 NULL」。
正確判斷與轉換模式
// ❌ 錯誤:直接 ToString() 會拋例外 (DBNull 無法轉型)
string name = row["Name"].ToString();
// ✅ 正確:先判斷 DBNull
object val = row["Name"];
string name = (val == DBNull.Value) ? null : val.ToString();
// ✅ 簡潔寫法 (C# 7.0+ 模式比對)
string name = row["Name"] is DBNull ? null : row["Name"].ToString();
// ✅ 擴充方法封裝 (推薦用於專案通用)
public static class DataRowExtensions
{
public static T GetValueOrDefault<T>(this DataRow row, string column, T defaultValue = default)
{
object val = row[column];
if (val == DBNull.Value || val == null) return defaultValue;
return (T)Convert.ChangeType(val, typeof(T));
}
}
// 使用:
int age = row.GetValueOrDefault("Age", 0);
string name = row.GetValueOrDefault("Name", "");
DateTime? birthday = row.GetValueOrDefault<DateTime?>("Birthday");
Convert.IsDBNull() vs DBNull.Value.Equals()
object val = row["Column"];
// 兩者等價,Convert.IsDBNull 內部即呼叫 DBNull.Value.Equals
bool isNull1 = Convert.IsDBNull(val);
bool isNull2 = DBNull.Value.Equals(val);
// 注意:val == DBNull.Value 也可行 (DBNull 覆寫 Equals),但模式比對更清晰
可為 Null 的實值型別 (Nullable<T> / T?)
語法與核心屬性
int? age = null; // 等同 Nullable<int> age = null;
int? age2 = 25;
Console.WriteLine(age.HasValue); // false
Console.WriteLine(age2.HasValue); // true
Console.WriteLine(age.Value); // ❌ InvalidOperationException (無值時存取 Value)
Console.WriteLine(age2.Value); // 25
// 安全取值:
int value = age.GetValueOrDefault(); // 0 (預設值)
int value2 = age.GetValueOrDefault(-1); // -1 (自訂預設)
int value3 = age ?? -1; // -1 (null 聯合運算子)
運算與比較規則
int? a = 10;
int? b = null;
Console.WriteLine(a + 5); // 15 (自動解包運算)
Console.WriteLine(b + 5); // null (傳播 null)
Console.WriteLine(a == b); // false
Console.WriteLine(b == null); // true
Console.WriteLine(a > b); // false (任何與 null 比較皆為 false)
// 處理邏輯:
int result = (a ?? 0) + (b ?? 0); // 10
實務場景決策表
| 場景 | 推薦判斷方式 | 程式碼範例 |
| 檢查字串是否有輸入內容 | string.IsNullOrWhiteSpace() | if (!string.IsNullOrWhiteSpace(txtInput.Text)) |
| 檢查字串非 null 且非空 | string.IsNullOrEmpty() | if (!string.IsNullOrEmpty(s)) |
| 資料庫字串欄位可能為 NULL | is DBNull + ?? 運算子 | string name = row["Name"] as string ?? ""; |
| 資料庫數值欄位可能為 NULL | Nullable<T> + GetValueOrDefault | int? age = row["Age"] as int?; int v = age ?? 0; |
| 方法選擇性參數 | Nullable<T> 或 預設參數 | void Foo(int? id = null) / void Foo(int id = 0) |
| 物件參考可能為 null | 模式比對 / ?. 運算子 | if (obj is MyClass m) { } / obj?.Method() |
現代 C# (7.0+) 空值處理新語法
1. Null 條件運算子 ?. 與 ?[]
// 傳統寫法
if (user != null && user.Address != null)
Console.WriteLine(user.Address.City);
// 現代寫法
Console.WriteLine(user?.Address?.City); // 任一為 null 結果為 null
// 陣列/集合安全存取
var first = list?[0]; // list 為 null 時回傳 null
2. Null 聯合運算子 ?? 與 ??=
// 提供預設值
string name = inputName ?? "Guest";
// C# 8.0: 僅在左邊為 null 時指派
cache ??= new Dictionary<string, object>();
3. 模式比對 is null / is not null
if (obj is null) { } // 比 obj == null 更安全 (避免運算子多載)
if (obj is not null) { } // C# 9.0
if (obj is string s) { } // 類型檢查 + 非 null 綁定
4. 可為 Null 參考型別 (NRT, C# 8.0+)
// 專案檔啟用:enable
// 非可為 null (預設)
string name = "John"; // 編譯器保證不為 null
// name = null; // ⚠️ 編譯警告 CS8600
// 明確可為 null
string? optional = null; // 允許 null
optional = "Value";
// 靜態分析輔助開發者預防 NullReferenceException
常見陷阱與除錯技巧
| 陷阱 | 錯誤現象 | 解決方案 |
| 字串未初始化直接存取 Length | NullReferenceException | 使用 string.IsNullOrEmpty() 或 ?.Length |
| DataRow 欄位為 DBNull 直接轉型 | InvalidCastException | 先判斷 DBNull.Value,使用 as 或模式比對 |
| Nullable<T> 直接存取 Value | InvalidOperationException | 先檢查 HasValue,或用 GetValueOrDefault/?? |
| 集合為 null 直接 foreach | NullReferenceException | 初始化為空集合 new List<T>(),或用 ?. 運算子 |
| 事件無訂閱者直接觸發 | NullReferenceException | 使用 ?.Invoke() 或預設委派 = delegate { } |
總結
C# 空值判斷的核心在於 區分「參考無物件」(null)、「空字串」("")、「資料庫 NULL"(DBNull)、「實值型別無值"(Nullable<T>) 四種語意不同的空值。現代 C# 提供 string.IsNullOrWhiteSpace、?.、??、is null、Nullable 參考型別 等強大工具,配合良好的防禦性程式設計習慣 (初始化集合、封裝資料庫存取、啟用 NRT),能從根源消除 90% 以上的空值相關 Bug。