检查空参数的最佳方法(保护条款)

Best way to check for null parameters (Guard Clauses)

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

的东西是很正常的
if (someArg == null)
{
    throw new ArgumentNullException(nameof(someArg));
}

if (otherArg == null)
{
    throw new ArgumentNullException(nameof(otherArg));
}

代码确实有点混乱。

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

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

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

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

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

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

    // other validation methods
}

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

这样只需要一行代码就可以验证一个参数:

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

空引用是您必须提防的一种麻烦。但是,他们不是唯一的。问题远不止于此,归结为:方法接受某种类型的实例,但它不能处理所有实例。

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

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

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

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 中引用。

有一个名为 SwissKnife. Install SwissKnife from nuget gallery 的 nuget 包。它为您提供了许多选项,从对参数进行空检查开始 Argument.IsNotNullOrEmpty(args,"args")SwissKnife.Diagnostics.Contracts 命名空间下以及选项 idoim 等等。您可以设置 Option<Class_Name> _someVar,然后检查 _someVar.IsSome_someVar.IsNone。这也有助于防止可为 nullable 类。希望这会有所帮助。

您可以试试我的 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));

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

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

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

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

ArgumentNullException.ThrowIfNull(someArg);

Ardalis 有一个优秀的 GuardClauses 库。

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

在 C# 8.0 及更高版本中,提供了新的帮助。 C# 8.0 引入了 non-nullable 引用类型(该功能在文档中被称为“可为空的引用类型”,有点令人困惑)。在 C# 8.0 之前,所有引用类型都可以设置为 null。但是现在有了 C# 8.0 和 'nullable' 项目设置,我们可以说引用类型默认为 non-nullable,然后让我们的变量和参数在 case-by-case 的基础上可以为 null。

所以目前,我们识别这样的代码:

public void MyFunction(int thisCannotBeNull, int? thisCouldBeNull)
{
    // no need for checking my thisCannotBeNull parameter for null here
}

如果您为 C# v8.0+ 项目设置 <Nullable>enable</Nullable>,您也可以这样做:

public void MyFunction(MyClass thisCannotBeNull, MyClass? thisCouldBeNull)
{
    // static code analysis at compile time checks to see if thisCannotBeNull could be null
}

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

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

    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 时,反射用于确定 属性 的名称,然后将显示在您的异常中

如果您想节省输入两次参数名称的时间,例如 Guard.AgainstNull(arg, nameof(arg));

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

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

也支持表单中的guard-and-set 我的属性 = Assign.IfNotNull(arg);

Nuget:YAGuard

免责声明:我是 YAGuard.

的作者

C# 11 介绍了执行此操作的最短方法(到目前为止)。您只需要在要检查是否为空的参数后紧跟 2 个感叹号 !!

之前:

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

使用 C# 11 或更高版本:

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

如果您调用 test(null),将抛出一个 ArgumentNullException 告诉您 someArg 是 null

Microsoft 在 2021 年初提到了它,但它并没有成为 C# 10 的一部分:A video on the 'Microsoft Developer' YouTube channel explaining the new feature

该功能已于 2022 年 2 月实施,see on GitHub


请注意:这是 C# 11 的一项功能,C# 11 尚未正式发布。它可能会在 2022 年 11 月与 .Net7 一起发布。

使用较新版本的 C#(C# 10,.NET6 将在几天内发布)你甚至可以这样做: ArgumentNullException.ThrowIfNull(someArg);

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