# 使用 CallerArgumentExpression 簡化參數檢核 在開發套件的時候,為了確保建構子或方法的參數符合預期,通常會進行 `null` 或空字串等參數檢核,為了簡化操作與統一錯誤訊息,我通常會寫個 `ExceptionUtils` 的靜態類別來進行檢查,範例如下: ```csharp 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` 是為了不需要同時傳入參數值和參數名稱,以簡化使用: ```csharp public void Method(string str) { ExceptionUtils.ThrowIfNullOrWhiteSpace(() => str); } ``` 但在 .NET 6 引入了 [Nullable reference type](https://learn.microsoft.com/zh-tw/dotnet/csharp/nullable-references) 的檢查機制。通常當我們執行 `null` 檢查後,編譯器就能辨識該變數不會是 `null`: ```csharp string ToLower(string? str) { if (str is null) { throw new ArgumentNullException(nameof(str)); } // 由於已經檢查過 null,編譯器不會再針對 str 發出 null 警告 return str.ToLower(); } ``` 但由於我的 `ExceptionUtils` 使用的是 `Expression`,而非直接檢查參數,因此無法在參數上加上 `[NotNull]` 來讓編譯器認知檢查後的參數不為 `null`。因此,我調整了程式碼如下: ```csharp 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 之後,官方提供了一些簡化的靜態檢查方法: ```csharp // .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]` 讓我想到之前和後輩借的書有提到它好像是用來自動取得參數名稱。 ```csharp public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) { if (argument is null) { Throw(paramName); } } ``` 因此,我就寫了以下程式進行測試: ```csharp 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 的檢查。 ###### tags: `.NET` `.NET Core & .NET 5+` `C#`