使用 CallerArgumentExpression 簡化參數檢核
在開發套件的時候,為了確保建構子或方法的參數符合預期,通常會進行 null 或空字串等參數檢核,為了簡化操作與統一錯誤訊息,我通常會寫個 ExceptionUtils 的靜態類別來進行檢查,範例如下:
public static class ExceptionUtils {
public static void ThrowIfNull<T>(Expression<Func<T?>> expression) {
_ = expression.Compile().Invoke()
?? throw new ArgumentNullException(GetMemberName(expression));
}
public static void ThrowIfNullOrWhiteSpace(Expression<Func<string?>> expression) {
string? value = expression.Compile().Invoke();
if (string.IsNullOrWhiteSpace(value)) {
throw new ArgumentException("不得為 Null 或空白字元。", GetMemberName(expression));
}
}
private static string GetMemberName<T>(Expression<Func<T>> expression) {
if (expression.Body is not MemberExpression expressionBody) {
throw new ArgumentException("Expression 表達式錯誤。", nameof(expression));
}
return expressionBody.Member.Name;
}
}這樣就可以用以下方式進行參數檢查,使用 Expression 是為了不需要同時傳入參數值和參數名稱,以簡化使用:
public void Method(string str) {
ExceptionUtils.ThrowIfNullOrWhiteSpace(() => str);
}但在 .NET 6 引入了 Nullable reference type 的檢查機制。通常當我們執行 null 檢查後,編譯器就能辨識該變數不會是 null:
string ToLower(string? str) {
if (str is null) {
throw new ArgumentNullException(nameof(str));
}
// 由於已經檢查過 null,編譯器不會再針對 str 發出 null 警告
return str.ToLower();
}但由於我的 ExceptionUtils 使用的是 Expression,而非直接檢查參數,因此無法在參數上加上 [NotNull] 來讓編譯器認知檢查後的參數不為 null。因此,我調整了程式碼如下:
public static class ExceptionUtils {
public static void ThrowIfNull<T>(Expression<Func<T>> expression, [DoesNotReturnIf(true)] bool isNull = true) {
_ = expression.Compile().Invoke()
?? throw new ArgumentNullException(GetMemberName(expression));
}
public static void ThrowIfNullOrWhiteSpace(Expression<Func<string?>> expression, [DoesNotReturnIf(true)] bool isNull = true) {
string? value = expression.Compile().Invoke();
if (string.IsNullOrWhiteSpace(value)) {
throw new ArgumentException("不得為 Null 或空白字元。", GetMemberName(expression));
}
}
private static string GetMemberName<T>(Expression<Func<T>> expression) {
return expression.Body is not MemberExpression expressionBody
? throw new ArgumentException("Expression 表達式錯誤。", nameof(expression))
: expressionBody.Member.Name;
}
}當中的參數 isNull 只是因為直接使用 [DoesNotReturn] 會出現 標記 [DoesNotReturn] 的方法不應傳回。 的警告。只好使用 [DoesNotReturnIf(true)] 搭配 isNull 這個無意義的參數來處理。 當然以上解法我一直不是很滿意,而在 .NET 6 和 .NET 7 之後,官方提供了一些簡化的靜態檢查方法:
// .NET 6 增加
ArgumentNullException.ThrowIfNull(object? argument, string? paramName = null);
// .NET 7 增加
ArgumentNullException.ThrowIfNullOrEmpty(string? argument, string? paramName = null);
ArgumentNullException.ThrowIfNullOrWhiteSpace(string? argument, string? paramName = null);
// .NET 7 增加
ArgumentException.ThrowIfNullOrEmpty(string? argument, string? paramName = null);
ArgumentException.ThrowIfNullOrWhiteSpace(string? argument, string? paramName = null);最近我看了 ThrowIfNull 的原始碼如下,當中的 [CallerArgumentExpression] 讓我想到之前和後輩借的書有提到它好像是用來自動取得參數名稱。
public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
if (argument is null) {
Throw(paramName);
}
}因此,我就寫了以下程式進行測試:
string? str = "";
Console.Write("未傳入 paramName 時,");
TestCallerArgumentExpression(str);
Console.Write("有傳入 paramName 時,");
TestCallerArgumentExpression(str, "str2");
void TestCallerArgumentExpression(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
Console.WriteLine("paramName:" + paramName);
}結果如下:
未傳入 paramName 時,paramName: str
有傳入 paramName 時,paramName: str2當未傳入 paramName 時,會自動使用傳入 argument 這個引數的變數名稱作為 paramName 的值。這種方式比我原先使用 Expression 的解法更簡潔,並且還能使用 [NotNull] 來支援 Nullable reference 的檢查。
異動歷程
- 2024-10-13 初版文件建立。
