检查空参数的最佳方法(保护子句)

Posted

技术标签:

【中文标题】检查空参数的最佳方法(保护子句)【英文标题】:Best way to check for null parameters (Guard Clauses) 【发布时间】:2015-05-24 22:42:12 【问题描述】:

例如,你通常不希望构造函数中的参数为空,所以看到类似的东西是很正常的

if (someArg == null)

    throw new ArgumentNullException(nameof(someArg));


if (otherArg == null)

    throw new ArgumentNullException(nameof(otherArg));

代码确实有点混乱。

有没有比这更好的方法来检查参数列表中的参数?

类似于“检查所有参数并在其中任何一个为空时抛出 ArgumentNullException 并为您提供为空的参数。

顺便说一句,关于重复的问题声明,这不是用属性或内置的东西标记参数,而是一些人称之为保护子句的东西,以保证对象接收初始化的依赖关系。

【问题讨论】:

Mark parameters as NOT nullable in C#/.NET? 的可能重复项 也许将它们全部放在一个对象数组中并使用 foreach 循环对其进行迭代?你需要这样的东西吗? 我们通常在方法的开头检查我们的参数,就像你的代码sn-p。不仅适用于 null,也适用于其他业务逻辑行为。只要您没有太多参数,我认为这没有任何问题。至少您可以轻松阅读方法的要求。 @JoJo 这是个糟糕的主意。您不希望创建一个旨在采用非常特定数量和类型的对象的方法,因为参数采用未知数量的未知类型的对象只是为了轻松检查它们是否为空。你正在通过创造一个更大的问题来解决一个问题。 @DanielMann 那么你还能做些什么来让它更短?我只是举个例子 【参考方案1】:

如果你的构造函数中的参数太多,你最好修改它们,但那是另一回事了。

为了减少样板验证代码,许多人编写了这样的 Guard 实用程序类:

public static class Guard

    public static void ThrowIfNull(object argumentValue, string argumentName)
    
        if (argumentValue == null)
        
            throw new ArgumentNullException(argumentName);
        
    

    // other validation methods

(您可以添加该 Guard 类可能需要的其他验证方法)。

因此只需要一行代码来验证一个参数:

    private static void Foo(object obj)
    
        Guard.ThrowIfNull(obj, "obj");
    

【讨论】:

只是一个小提示,您也可以使用nameof 运算符而不是硬编码参数名称,例如Guard.ThrowIfNull(obj, nameof(obj)) @DanielSmith 有什么理由不将 nameof(obj) 移到 ThrowIfNull 方法内部并且在使用时只有一个参数? @aufty 因为nameof(obj) 只返回“obj”。丹尼尔的意思是nameof(howeverYourParameterIsNamed)。在ThrowIfNull,你必须写nameof(argumentValue)(或者你选择命名参数),所以你总是会得到NullReferenceException:argumentValue was null...不是很有启发性。【参考方案2】:

空引用是您必须防范的一种麻烦。但是,他们并不是唯一的。问题比这更广泛,归结为:方法接受某种类型的实例,但它不能处理所有实例。

换句话说,方法的域大于它处理的值集。然后使用保护子句断言实际参数不属于方法域的无法处理的“灰色区域”。

现在,我们有空引用,作为一个通常超出可接受值集的值。另一方面,经常发生集合中的一些非空元素也是不可接受的(例如空字符串)。

在这种情况下,可能会证明方法签名过于宽泛,这表明存在设计问题。这可能会导致重新设计,例如定义一个子类型,通常是一个派生接口,它限制方法的域并使一些保护子句消失。你可以在这篇文章中找到一个例子:Why do We Need Guard Clauses?

【讨论】:

我同意你在这里所说的,但我不知道如何将其应用于所述问题,以便在编译时禁止空值,因为 c# 中的引用类型始终可以为空。 @TimAbell OP 并没有要求这样做;无论如何,C# 8 现在引入了可为空的引用类型,因此您可以强制编译器报告对可能为空的引用的所有可疑访问。其他可能性是依赖于 C# 本身不支持的可选对象。【参考方案3】:
public static class Ensure

    /// <summary>
    /// Ensures that the specified argument is not null.
    /// </summary>
    /// <param name="argumentName">Name of the argument.</param>
    /// <param name="argument">The argument.</param>
    [DebuggerStepThrough]
    [ContractAnnotation("halt <= argument:null")]        
    public static void ArgumentNotNull(object argument, [InvokerParameterName] string argumentName)
    
        if (argument == null)
        
            throw new ArgumentNullException(argumentName);
        
    

用法:

// C# < 6
public Constructor([NotNull] object foo)

    Ensure.ArgumentNotNull(foo, "foo");
    ...


// C# >= 6
public Constructor([NotNull] object bar)

    Ensure.ArgumentNotNull(bar, nameof(bar));
    ...

DebuggerStepThroughAttribute 非常方便,因此在调试时出现异常(或者当我在异常发生后附加调试器时)我不会在 ArgumentNotNull 方法中结束,而是在调用方法中结束空引用 actually 发生了。

我正在使用ReSharper Contract Annotations。

ContractAnnotationAttribute 确保我不会拼错 参数("foo"),如果我重命名它也会自动重命名 foo 符号。 NotNullAttribute 帮助 ReSharper 进行代码分析。因此,如果我执行new Constructor(null),我会收到 ReSharper 的警告,指出这将导致异常。 如果您不喜欢直接注释代码, 您也可以使用 external XML-files 执行相同的操作,您可以将其部署到您的库中,并且用户可以选择在他们的 ReSharper 中引用。

【讨论】:

你知道如何将此代码重写为Code Contract,而不是ResharperAnnotation。甚至可能吗?或者我没看懂。提前感谢您的帮助 代码合约基本都是dead。我不会再使用它们了。如果您使用 .NET Core 或计划切换到它,我会使用新的 nullable reference types 功能。 在 LinksPlatform 我们有Platform.Exceptions library,其中包含单行检查器方法的类似实现。如果您需要额外的检查,您可以使用扩展方法扩展我们的实现,或者要求我们将这些检查添加到我们的库中。【参考方案4】:

有一个名为 SwissKnife 的 nuget 包。从nuget gallery 安装SwissKnife。它为您提供了许多选项,从检查参数的空值开始 Argument.IsNotNullOrEmpty(args,"args")SwissKnife.Diagnostics.Contracts 命名空间下以及选项 idoim 等等。您可以设置Option&lt;Class_Name&gt; _someVar,然后检查是_someVar.IsSome 还是_someVar.IsNone。这也有助于对抗可为空的类。希望这会有所帮助。

【讨论】:

【参考方案5】:

你可以试试我的 Heleonix.Guard 库,它提供了保护功能。

你可以像下面这样写保护子句:

// C# 7.2+: Non-Trailing named arguments
Throw.ArgumentNullException(when: param.IsNull(), nameof(param));

// OR

// Prior to C# 7.2: You can use a helper method 'When'
Throw.ArgumentNullException(When(param.IsNull()), nameof(param));

// OR
Throw.ArgumentNullException(param == null, nameof(param));

// OR
Throw.ArgumentNullException(When (param == null), nameof(param));

它提供了许多现有异常的抛出,您可以为自定义异常编写自定义扩展方法。此外,该库还引用了带有诸如IsNullIsNullOrEmptyOrWhitespaceIsLessThan 等谓词扩展的“Heleonix.Extensions”库,以检查您的参数或变量是否与所需值相符。与其他一些具有流畅接口的保护库不同,这些扩展不会生成中间对象,而且由于实现非常简单,因此它们是高性能的。

【讨论】:

我认为所有参数都被评估是否正确,即使条件为假?对于问题中的简单案例来说,这不是问题,但用户在其他情况下使用此库时需要注意这一点。 每个参数都应该有单独的检查。 IE。对于问题中的情况,如下所示:Throw.ArgumentNullException(when: someArg.IsNull(), nameof(someArg)); Throw.ArgumentNullException(when: otherArg.IsNull(), nameof(otherArg)); 如果第一个表达式 someArg.IsNull() 的评估是 true,则抛出异常,因此不评估第二个表达式 otherArg.IsNull() 等等。 我的意思是,nameof(someArg) 总是被评估,即使someArg.IsNull() 返回 false,这与 if/then 构造不同。在这种情况下,这可能没问题,但如果不是简单的nameof(),我们有一些带有副作用(包括时间效应)的东西,这可能是不可取的。 对。这就是为什么抛出异常应该像一个简单的单行表达式。如果有一些 ie 多行逻辑,尤其是有副作用,最好使用常规的if 语句,因为该逻辑更具可读性(对于我来说,我认为它是你算法的一部分)跨度> 【参考方案6】:

使用较新版本的 C# 语言,您无需额外的库或额外的方法调用即可编写此代码:

_ = someArg ?? throw new ArgumentNullException(nameof(someArg));
_ = otherArg ?? throw new ArgumentNullException(nameof(otherArg));

从 .NET6 开始你也可以这样写:

ArgumentNullException.ThrowIfNull(someArg);

【讨论】:

如果你还没有遇到discards 并且像我一样想知道下划线。 即将推出的 .NET6 更新更短的方式:ArgumentNullException.ThrowIfNull(someArg); (***.com/a/69836300/2315856)【参考方案7】:

Ardalis 拥有出色的 GuardClauses 库。

很好用Guard.Against.Null(message, nameof(message));

【讨论】:

【参考方案8】:

在 C# 8.0 及更高版本中,提供了新的帮助。 C# 8.0 引入了不可为空的引用类型(文档中称为“可空引用类型”的功能有点令人困惑)。在 C# 8.0 之前,所有引用类型都可以设置为 null。但是现在有了 C# 8.0 和 'nullable' 项目设置,我们可以说引用类型默认是不可为空的,然后根据具体情况使我们的变量和参数可以为空。

因此,目前我们可以识别这样的代码:

public void MyFunction(int thisCannotBeNull, int? thisCouldBeNull)

    // no need for checking my thisCannotBeNull parameter for null here

如果你为你的 C# v8.0+ 项目设置了&lt;Nullable&gt;enable&lt;/Nullable&gt;,你也可以这样做:

public void MyFunction(MyClass thisCannotBeNull, MyClass? thisCouldBeNull)

    // static code analysis at compile time checks to see if thisCannotBeNull could be null

空值检查是在编译时使用静态代码分析完成的。因此,如果您以一种意味着可能出现空值的方式对其进行编码,您将收到编译器警告(如果需要,您可以将其升级为错误)。因此,很多(但不是全部)需要对空参数进行运行时检查的情况可以根据您的代码作为编译时检查来处理。

【讨论】:

【参考方案9】:

我发现的最简单的方法是受到 Dapper 使用匿名类型的启发。 我编写了一个使用匿名类型来获取属性名称的 Guard 类。 守卫本身如下

    public class Guard
    
        public static void ThrowIfNull(object param) 
        
            var props = param.GetType().GetProperties();
            foreach (var prop in props) 
            
                var name = prop.Name;
                var val = prop.GetValue(param, null);

                _ = val ?? throw new ArgumentNullException(name);
            
        
    

然后你就这样使用它

...
        public void Method(string someValue, string otherValue)
        
            Guard.ThrowIfNull(new  someValue, otherValue ); 
        
...

当 ArgumentNullException 被抛出时,反射用于确定属性的名称,然后将显示在您的异常中

【讨论】:

【参考方案10】:

如果您想保存两次输入参数名称,例如 Guard.AgainstNull(arg, nameof(arg));

查看YAGuard,您可以在其中写信 Guard.AgainstNull(arg);

无需在guard子句中指定参数名称,但在抛出的参数中,名称被正确解析。

它还支持表单中的guard-and-set MyProperty = 赋值.IfNotNull(arg);

Nuget:YAGuard

免责声明:我是YAGuard的作者。

【讨论】:

【参考方案11】:

C# 10 介绍了执行此操作的最短方法(到目前为止)。您只需要在要检查 null 的参数后面加上 2 个感叹号 !!

之前:

public void test(string someArg)
    if (someArg == null)
    
        throw new ArgumentNullException(nameof(someArg));
    
    // other code

使用 C# 10 或更高版本:

public void test(string someArg!!)
    // other code

如果您调用 test(null),则会抛出 ArgumentNullException,告诉您 someArg 是 null

来源:A video on the 'Microsoft Developer' YouTube channel explaining the new feature


请注意:这是 C# 10 的一项功能,C# 10 尚未正式发布。它将于 2021 年 11 月与 .Net6 一起发布 (according to Microsoft)。

【讨论】:

【参考方案12】:

使用更新版本的 C#(C# 10,.NET6 将在几天内发布)您甚至可以执行以下操作: ArgumentNullException.ThrowIfNull(someArg);

文档:https://docs.microsoft.com/en-us/dotnet/api/system.argumentnullexception.throwifnull?view=net-6.0

【讨论】:

以上是关于检查空参数的最佳方法(保护子句)的主要内容,如果未能解决你的问题,请参考以下文章

检查数据表中是不是包含空值的最佳方法

SqlDataReader 检查空值的最佳方法 -sqlDataReader.IsDBNull vs DBNull.Value

可空类型:在c#中检查null或0的最佳方法

如何在保护子句中使用“in”运算符?

如果不是 Null,则检查输入参数并在 SQL Server 的 where 中使用它

可空类型:在 c# 中检查空或零的更好方法