从另一个方法检查是否为空。

huangapple go评论75阅读模式
英文:

Checking for null from another method

问题

在C#中,当我尝试在当前方法之外检查null时,编译器在编译时似乎无法理解发生了什么。例如,考虑这个方法:

public static class Guard {
    public static void ThrowIfNull(params object?[] args) {
        foreach(var arg in args) {
            ArgumentNullException.ThrowIfNull(arg);
        }
    }
}

当我使用这个方法来验证另一个方法的参数时,比如:

public void TestCase(string? name, string? email) {
    Guard.ThrowIfNull(name, email);
    
    string _name = name;
    string _email = email;
}

编译器无法理解这个null检查,我仍然会得到可空警告。

这里的问题有几个:

  1. 我不希望在null检查之后使用 ! 来抑制null警告。
  2. ArgumentNullException.ThrowIfNull(arg); 移到方法内部实际上会使代码变得丑陋。
  3. 我无法在参数数组上使用 NotNull 特性,因为C#无法理解我实际上是要为数组的每个索引指定NotNull。

那么这个问题应该如何解决呢?

英文:

In C#, when I try to check for nulls outside the current method, somehow the compiler does not understand what is going on at compile time. For instance, consider this method:

public static class Guard {
     public static void ThrowIfNull(params object?[] args) {
         foreach(var arg in args) {
             ArgumentNullException.ThrowIfNull(arg);
         }
     }
}

When I use this method to actually validate parameters of another method, let's say:

public void TestCase(string? name, string? email) {
    Guard.ThrowIfNull(name, email);
    
    string _name = name;
    string _email = email;
}

The compiler does not understand this null check, and I still get the nullable warning.

The problem here is a few things:

  1. I am not interested in null suppression by using ! after the null check
  2. Moving ArgumentNullException.ThrowIfNull(arg); inside the method actually makes the code ugly
  3. I cannot use the NotNull attribute on the params array because C# does not understand that I actually mean NotNull for each index of the array

Now how should this problem be solved?

答案1

得分: 3

首先,数组更难检查是否为空,因此你应该考虑另一种解决方案。

其次,我个人更喜欢直接抛出异常而不是使用函数。这是因为可以看到哪个参数为null(即我想看到它的名称)。类似于以下方式:

public void TestCase(string? name, string? email)
{
    string _name = name ?? throw new ArgumentNullException(nameof(name));
    string _email = email ?? throw new ArgumentNullException(nameof(email));
}

最后,看一下ArgumentNullException.ThrowIfNull(arg)是如何内部编写的。这会给你一个提示:

public static void ThrowIfNull(
    [NotNull] object? argument, 
    [CallerArgumentExpression("argument")] string? paramName = null
    )
{
    if (argument is null)
    {
        Throw(paramName);
    }
}

也就是说,[NotNull] 属性完成了你需要的工作。

话虽如此,我会将你的代码重写如下:

public static class Guard
{
    public static void ThrowIfNull([NotNull] object? arg)
    {
        ArgumentNullException.ThrowIfNull(arg);
    }

    public static void ThrowIfNull([NotNull] object? arg1, [NotNull] object? arg2)
    {
        ArgumentNullException.ThrowIfNull(arg1);
        ArgumentNullException.ThrowIfNull(arg2);
    }

    public static void ThrowIfNull([NotNull] object? arg1, [NotNull] object? arg2, [NotNull] object? arg3)
    {
        ArgumentNullException.ThrowIfNull(arg1);
        ArgumentNullException.ThrowIfNull(arg2);
        ArgumentNullException.ThrowIfNull(arg3);
    }

    // ...等等...
}

在测试的代码片段中不再有警告:

public void TestCase(string? name, string? email)
{
    Guard.ThrowIfNull(name, email);

    string _name = name;    //没有警告
    string _email = email;  //没有警告
}
英文:

First off, arrays are harder to check for nullability, hence you should favor another solution.

Secondly, I personally prefer to throw "in place" instead to use a function. That's because is useful to see which parameter is null (i.e. I want the see its name). Something like this:

public void TestCase(string? name, string? email)
{
    string _name = name ?? throw new ArgumentNullException(nameof(name));
    string _email = email ?? throw new ArgumentNullException(nameof(email));
}

Finally, have a look on how the ArgumentNullException.ThrowIfNull(arg) is internally written. That will give a hint:

    public static void ThrowIfNull(
        [NotNull] object? argument, 
        [CallerArgumentExpression("argument")] string? paramName = null
        )
    {
        if (argument is null)
        {
            Throw(paramName);
        }
    }

That is, the [NotNull] attribute does the job you need.

That being said, I'd rewrite your code as follows:

public static class Guard
{
    public static void ThrowIfNull([NotNull] object? arg)
    {
        ArgumentNullException.ThrowIfNull(arg);
    }

    public static void ThrowIfNull([NotNull] object? arg1, [NotNull] object? arg2)
    {
        ArgumentNullException.ThrowIfNull(arg1);
        ArgumentNullException.ThrowIfNull(arg2);
    }

    public static void ThrowIfNull([NotNull] object? arg1, [NotNull] object? arg2, [NotNull] object? arg3)
    {
        ArgumentNullException.ThrowIfNull(arg1);
        ArgumentNullException.ThrowIfNull(arg2);
        ArgumentNullException.ThrowIfNull(arg3);
    }

    // ...etc...
}

The piece under test shows no more warning:

    public void TestCase(string? name, string? email)
    {
        Guard.ThrowIfNull(name, email);

        string _name = name;    //no warning
        string _email = email;  //no warning
    }

答案2

得分: 1

不幸的是(正如其他人指出的),你不能用 params 实现这个。

我猜你想避免“丑陋”的代码,在这种代码中,你必须为每个参数分别调用 ArgumentNullException.ThrowIfNull()

在这种情况下,你可以为你想要检查的参数数量编写一个单独的方法。但遗憾的是,如果你使用 [CallerArgumentExpression] 来指定参数名称,你不能重载这些方法,因为编译器会选择具有最少参数而不是你想要的重载。

然而,考虑到这些限制,你可以编写类似这样的代码:

public static class MyArgumentNullException
{
    public static void ThrowIf1Null([NotNull] object? arg, [CallerArgumentExpression("arg")] string? argName = null)
    {
        if (arg == null) 
            throw new ArgumentNullException(argName);
    }

    // ...(其他方法的翻译被省略)...

}

然后你可以这样编写代码:

public static class Program
{
    public static void Main()
    {
        // This throws with message:
        // "Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'u')"
        Test("a", "b", null!);
    }

    public static void Test(string s, string t, string u)
    {
        MyArgumentNullException.ThrowIf3Null(s, t, u);

        Console.WriteLine(s.Length + t.Length + u.Length);
    }
}
英文:

Unfortunately (as others have pointed out) you cannot achieve this with params.

I guess you want to avoid "ugly" code where you have to call ArgumentNullException.ThrowIfNull() for each parameter separately.

In this case you could write a separate method for each number of arguments that you want to check. Alas, you can't overload these methods if you are using [CallerArgumentExpression] to specify the argument names because the compiler will select the overload with the fewest parameters rather than the one you want.

However, given those restrictions you can write something like this:

public static class MyArgumentNullException
{
    public static void ThrowIf1Null([NotNull] object? arg, [CallerArgumentExpression("arg")] string? argName = null)
    {
        if (arg == null) 
            throw new ArgumentNullException(argName);
    }

    public static void ThrowIf2Null(
        [NotNull]                          object? arg1,
        [NotNull]                          object? arg2,
        [CallerArgumentExpression("arg1")] string? argName1 = null,
        [CallerArgumentExpression("arg2")] string? argName2 = null)
    {
        if (arg1 == null)
            throw new ArgumentNullException(argName1);

        if (arg2 == null)
            throw new ArgumentNullException(argName2);
    }

    public static void ThrowIf3Null(
        [NotNull]                          object? arg1,
        [NotNull]                          object? arg2,
        [NotNull]                          object? arg3,
        [CallerArgumentExpression("arg1")] string? argName1 = null,
        [CallerArgumentExpression("arg2")] string? argName2 = null,
        [CallerArgumentExpression("arg3")] string? argName3 = null)
    {
        if (arg1 == null)
            throw new ArgumentNullException(argName1);

        if (arg2 == null)
            throw new ArgumentNullException(argName2);

        if (arg3 == null)
            throw new ArgumentNullException(argName3);
    }

    // And so on for all the paraneters you want.
}

Then you can write code like this:

public static class Program
{
    public static void Main()
    {
        // This throws with message:
        // "Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'u')"
        Test("a", "b", null!);
    }

    public static void Test(string s, string t, string u)
    {
        MyArgumentNullException.ThrowIf3Null(s, t, u);

        Console.WriteLine(s.Length + t.Length + u.Length);
    }
}

答案3

得分: -1

问题的最终答案是,无法向C#编译器定义空检查是在其他方法中进行的。这导致结论:使用可空引用类型实际上给代码带来了一些问题:

  1. 无法集中处理空检查。没有这种功能,所有方法都必须单独检查空值,使代码变得更长更难看。
  2. 使用!运算符抑制空警告是毫无意义的。
  3. 如果不在正确的位置执行空检查(正如我之前解释的那样,这会使代码变得难看),安全扫描和代码分析工具可能会生成误报。

这个功能在我发布这个答案时似乎要么设计不良,要么不完整。

英文:

The final answer to the question is that, there is no way to define to the C# compiler that the null check is taking place in the other method. This leads to the conclusion that using nullable reference types actually brings a few problems to the code:

  1. Null checks cannot be handled centrally. Without such feature, all methods have to individually check for nulls which makes the code lengthier and uglier.
  2. It is absolutely pointless to suppress null warnings using the ! operator.
  3. False positive reports can be generated by security scanning and code analysis tools if null checks are not done in the right place (which as I explained earlier makes the code ugly).

This feature seems to be either badly designed or incomplete at the time I posted this answer.

huangapple
  • 本文由 发表于 2023年6月19日 11:23:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/76503432.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定