「空參考」被稱為「十億美元的錯誤」。C# 提供兩套機制處理「缺少值」:可為 Null 的實值型別(Nullable<T>/T?)與 C# 8.0+ 可為 Null 的參考型別(string?、List<T>?)。正確使用這兩套機制,能在編譯期消除大量 NullReferenceException。
實值型別(int、bool、DateTime、struct)本身不可為 null。Nullable<T> 為其包裝一層,增加「無值」狀態:
// 語法糖:T? 等同 Nullable
int? count = null;
int? count2 = 42;
bool? flag = null;
DateTime? birthday = null;
// 底層結構
// public struct Nullable where T : struct {
// public bool HasValue { get; }
// public T Value { get; } // HasValue=false 時擲出例外
// public T GetValueOrDefault() => HasValue ? Value : default;
// public T GetValueOrDefault(T defaultValue) => HasValue ? Value : defaultValue;
// }
int? x = 5;
int? y = null;
x.HasValue; // true
y.HasValue; // false
x.Value; // 5
// y.Value; // InvalidOperationException
x.GetValueOrDefault(); // 5
y.GetValueOrDefault(); // 0 (int 預設值)
y.GetValueOrDefault(-1); // -1
// 隱式轉換:T -> T?
int a = 10;
int? b = a; // 隱式
// 顯式轉換:T? -> T (可能擲出)
int c = (int)x; // 或 x.Value
int d = y ?? -1; // 空合併運算子 (推薦)
??:提供預設值int? nullable = null;
int value = nullable ?? 42; // 42
// 鏈式
string? s1 = null, s2 = null, s3 = "hello";
string result = s1 ?? s2 ?? s3 ?? "default"; // "hello"
// 條件存取配合
var length = user?.Profile?.Name?.Length ?? 0;
??=`:唯賦值當為 nullList? list = null;
list ??= new List(); // 只有 null 時初始化
list.Add("item");
C# 8.0+ 編譯器追蹤參考型別的 null 狀態,需在專案啟用:
enable
nullable
或檔案層級:#nullable enable / #nullable disable / #nullable restore。
string? nullableString = null; // 可為 null
string nonNullString = "hello"; // 非 null (編譯器強制初始化)
List? maybeList = null; // 可為 null
List list = new(); // 非 null
| 程式碼 | 編譯器判斷 | 警告/錯誤 |
|---|---|---|
string s = null; | 非 null 型別指派 null | CS8600/CS8625 |
string? s = null; s.Length | 可能為 null 解參考 | CS8602 |
string s = GetString(); s.Length | 若 GetString 回傳 string? 則警告 | CS8602 |
string s = GetString()!; | Null-forgiving 運算子,開發者承擔風險 | 無 (壓制警告) |
string s = GetString() ?? "default"; | 空合併後確定非 null | 無 |
// 1. 防禦性檢查
public void Process(string? input) {
if (input is null) throw new ArgumentNullException(nameof(input));
// 此處 input 判斷為非 null
Console.WriteLine(input.Length);
}
// 2. 模式比對
if (input is { Length: > 0 } str) { // 非 null 且長度 > 0
Console.WriteLine(str);
}
// 3. 建構子初始化非 null 屬性
public class User {
public string Name { get; } // 非 null
public User(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));
}
// 4. 必要屬性
public class Config {
public required string ConnectionString { get; init; } // C# 11+
}
// T 為實值型別,可用 T?
public struct NullableWrapper where T : struct { }
// T 為參考型別,T? 表示可為 null
public class Repository where T : class {
public T? FindById(int id) => null; // 回傳可為 null
}
// notnull 約束:T 必須為非 null 型別 (實值型別或非 null 參考型別)
public class Cache where TKey : notnull { }
為舊版程式庫/外部 API 提供 null 狀態提示:
// 回傳值可能為 null
[return: MaybeNull] public string GetOrDefault(int id) => null;
// 參數不應為 null
public void SetName([NotNull] string name) { }
// 參數可能為 null
public void Log([AllowNull] string message) { }
// 條件式 null 狀態
public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) { ... }
<Nullable>enable</Nullable>:從第一行程式碼受益編譯期保護。! 運算子:只有確定安全(如 DI 容器注入、單元測試模擬)才使用。? 表達意圖,減少呼叫端猜測。T?、參考型別用 T? (啟用功能時),語意統一。x ?? default 簡潔且編譯器理解結果非 null。List 而非 List<object>,泛型保留 null 狀態資訊。Nullable<T> 是類別還是結構?A: 是結構體(struct),所以本身不可為 null,佔用記憶體為 T + bool。
double.NaN vs double? 代表「無值」?A: double? 語意更清晰、型別安全、可用 HasValue 檢查。NaN 仍是有效 double 值,參與運算會傳播,易造成邏輯錯誤。
A: 1) 專案檔加 <Nullable>enable</Nullable> 2) 逐檔 #nullable disable 暫時關閉 3) 修復警告 4) 移除 disable 5) 加 WarningsAsErrors。
A: string?[]:陣列本身可為 null,元素可為 null。string[]?:陣列可為 null,元素非 null。List<string?>:集合非 null,元素可為 null。