ArgumentNullException – 如何简化?

我注意到这个代码在我的构造函数中出现了很多:

if (someParam == null) throw new ArgumentNullException("someParam"); if (someOtherParam == null) throw new ArgumentNullException("someOtherParam"); ... 

我有一些构造函数,其中注入了几个东西,并且必须都是非null。 谁能想到一种简化这种方法的方法? 我唯一能想到的是以下几点:

 public static class ExceptionHelpers { public static void CheckAndThrowArgNullEx(IEnumerable<KeyValuePair> parameters) { foreach(var parameter in parameters) if(parameter.Value == null) throw new ArgumentNullException(parameter.Key); } } 

但是,使用它会是这样的:

 ExceptionHelper.CheckAndThrowArgNullEx(new [] { new KeyValuePair("someParam", someParam), new KeyValuePair("someOtherParam", someOtherParam), ... }); 

…这并没有真正帮助简化代码。 Tuple.Create()而不是KVP不起作用,因为Tuple的GTP不协变(即使IEnumerable的GTP是)。 有任何想法吗?

C#7的更新

您可以将throw表达式与null合并运算符一起使用。 以下是该页面的示例:

 public string Name { get => name; set => name = value ?? throw new ArgumentNullException(paramName: nameof(value), message: "New name must not be null"); } 

原始答案

就个人而言,我使用ThrowIfNull扩展方法。 我不知道该归功于谁,但我绝对没有发明它 。 这很好,因为你可以使用返回值进行赋值:

 public static T ThrowIfNull(this T argument, string argumentName) { if (argument == null) { throw new ArgumentNullException(argumentName); } return argument; } 

用法:

 this.something = theArgument.ThrowIfNull("theArgument"); // or in C# 6 this.something = theArgument.ThrowIfNull(nameof(theArgument)); 

(虽然有些人认为在null实例上调用扩展方法很奇怪)

如果你真的想一次检查多个参数,如果你使用params签名,那么你的例子可能会更精简:

 public static void CheckAndThrowArgNullEx(params object[] argsAndNames) { for (int i = 0; i < argsAndNames.Length; i += 2) { if (argsAndNames[i] == null) { string argName = (string)argsAndNames[i + 1]; throw new ArgumentNullException(argName); } } } 

用法是:

 CheckAndThrowArgNullEx(arg1, "arg1", arg2, "arg2"); // or in C# 6 CheckAndThrowArgNullEx(arg1, nameof(arg1), arg2, nameof(arg2)); 

第二个想法,正如KeithS在评论中提到的那样,将它实现为一组重载可能会更好,而不是像这样使用params object[]

 static void Check(object arg1, string arg1Name) { ... } static void Check(object arg1, string arg1Name, object arg2, string arg2Name) { ... } // and so on... 

有几种方法可以解决这个问题。

选项A:

将您的function分解为两个 – validation和实现(您可以在Jon Skeet的EduLinq中看到此示例)。

选项B:

使用期望参数为非null的代码协定 。

选项C:

使用面向方面的技术(如代码编织)将这些检查提取到一个方面。 (正如J托雷斯回答的那样 )。

选项D:

使用Spec# ,如CodeInChaos所评论 。

选项E:

???

大多数人的Upticks; 你的答案有助于我最终得到的解决方案,其中包含了点点滴滴但最终与所有这些解决方案不同。

我创建了一些静态方法,它们处理特定forms的lambda表达式( 编辑 – 小变化;方法不能是通用的,或者它们需要所有表达式返回相同的类型.Func很好,而且有一个额外的条件在GetName方法中展开强制转换):

 public static class ExpressionReader { ///  /// Gets the name of the variable or member specified in the lambda. ///  /// The lambda expression to analyze. /// The lambda MUST be of the form ()=>variableName. ///  public static string GetName(this Expression> expr) { if (expr.Body.NodeType == ExpressionType.MemberAccess) return ((MemberExpression) expr.Body).Member.Name; //most value type lambdas will need this because creating the //Expression from the lambda adds a conversion step. if (expr.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)expr.Body).Operand.NodeType == ExpressionType.MemberAccess) return ((MemberExpression)((UnaryExpression)expr.Body).Operand) .Member.Name; throw new ArgumentException( "Argument 'expr' must be of the form ()=>variableName."); } } public static class ExHelper { ///  /// Throws an ArgumentNullException if the value of any passed expression is null. ///  /// The lambda expressions to analyze. /// The lambdas MUST be of the form ()=>variableName. ///  public static void CheckForNullArg(params Expression>[] exprs) { foreach (var expr in exprs) if(expr.Compile()() == null) throw new ArgumentNullException(expr.GetName()); } } 

……可以这样使用:

 //usage: ExHelper.CheckForNullArg(()=>someParam, ()=>someOtherParam); 

这样可以在没有第三方工具的情况下将样板减少到一行。 ExpressionReader以及exception生成方法可以处理在调用者中编译的form()=> variableName的任何lambda,这意味着它至少适用于局部变量,参数,实例字段和实例属性。 我没有检查它是否适用于静力学。

 public class TestClass { public TestClass() { this.ThrowIfNull(t=>t.Str, t=>t.Test); //OR //this.ThrowIfNull(t => tX) // .ThrowIfNull(t => t.Test); } string Str = ""; public TestClass Test {set;get;} } public static class SOExtension { public static T ThrowIfNull(this T target, params Expression>[] exprs) { foreach (var e in exprs) { var exp = e.Body as MemberExpression; if (exp == null) { throw new ArgumentException("Argument 'expr' must be of the form x=>x.variableName"); } var name = exp.Member.Name; if (e.Compile()(target) == null) throw new ArgumentNullException(name,"Parameter [" + name + "] can not be null"); } return target; } } 

如果您不反对第三方实用程序,PostSharp提供了注入此类validation的简洁方法。 此博客文章提供了您的问题的解决方案。

更新:请参阅PostSharp 3中的新validation参数function

扩展方法怎么样?

 public static void ThrowExceptionIfNull(this object argument, string argumentName) { if(argument == null) throw new ArgumentNullException(argumentName); } 

然后你的代码至少读得更流畅一点:

 someParam.ThrowExceptionIfNull("someParam"); 

否则,我会同意其他人分割function或使用AOP(即PostSharp)

已经有很多有效的解决方案,但这是我的看法:

 using System.Diagnostics; using System.Reflection; public SomeConstructor(int? par1, int? par2, string par3) { CheckThrowNull(par1, par2, par3); //rest of constructor code... } /// Values must be given in order  public static void CheckThrowNull(params object[] values) { StackTrace stackTrace = new StackTrace(); ParameterInfo[] parameters = stackTrace.GetFrame(1).GetMethod().GetParameters(); //get calling method's parameters (or constructor) if (parameters.Length != values.Length) { throw new ArgumentException("Incorrect number of values passed in"); } for (int i = 0; i < parameters.Length; i++) { if (values[i] == null) { //value was null, throw exception with corresponding parameter name throw new ArgumentNullException(parameters[i].Name); } } } 

一般的想法是建立两个并行数组,一个是ParameterInfo类型,另一个是包含参数值。 后者必须传入,因为参数值不容易(并且我认为不可能)通过reflection获得。 为了给予应有的信用,我在这里找到了如何获得调用方法: http : //www.csharp-examples.net/reflection-calling-method-name/

就个人而言,我不喜欢使用System.Diagnosics,除了调试,所以我会稍微修改一下,调用代码为:

 CheckThrowNull(MethodBase.GetCurrentMethod(), par1, par2, par3); 

而方法是

 CheckThrowNull(MethodBase method, params object[] values) { ParameterInfo[] parameters = method.GetParameters(); //rest of code same } 

下端是有点不可扩展的,并且不能轻易地检查是否只有一些参数为null。

试试这个:一行。

 accounts = accounts ?? throw new ArgumentNullException(nameof(accounts)); 

另外,使用nameof() ,如果变量被重命名,你不必追捕所有“变量”,让nameof()这样做。

那么,样板很难避免。 您可以切换到使用Bertrand Meyers的Eiffel编程语言和EiffelStudio而不是C#和Visual Studio,并开始练习“ 按合同设计 ”。

如今,Eiffel完全符合CLR标准。

我写了基准应用程序,提取参数名称的多种变体(通过匿名类+reflection / MemberExpression / Func /等)

Github链接到基准源: https : //github.com/iXab3r/NullCheckCompetition

我得到的结果显示,最快的方法是使用匿名类。

.NET 40 / X64

失败(即参数IS为null并执行名称提取方法)

  • 失败 AnonymousClass 67.87 ns
  • 失败 DoubleLambda 643.98 ns
  • 失败 LazyAnonymousClass 69.36 ns
  • RawCheck 失败 1.08 ns
  • 失败 SingleLambda 643.27 ns

成功(即参数不为空)

  • SuccessAnonymousClass 6.33 ns
  • SuccessDoubleLambda 8.48 ns
  • SuccessLazyAnonymousClass 8.78 ns
  • SuccessRawCheck 1.08 ns
  • SuccessSingleLambda 628.28 ns

实际上,可以从lambda表达式中检索参数名称,而无需通过Expression类型。 这是如何做到的:

 static void SampleMethod(string arg1) { ThrowIfNull(() => arg1); // continue to other normal stuff here... } public static void ThrowIfNull(Func lambda) where T : class { if (lambda() == null) { throw new ArgumentNullException(lambda.Target.GetType().GetFields()[0].Name); } } 

我认为上面的大部分内容都没问题,但它们都没有真正改善你已有的东西,所以我只想去KIS,Keep It Simple,这就是你的开始。

它干净,极易读取且速度快。 它唯一有点长