C#中的空参数检查

91
在 C# 中,除了提供更好的错误信息外,为每个函数添加参数空检查还有没有其他好的理由?显然,使用 s 的代码无论如何都会抛出异常。这样的检查会使代码变慢并难以维护。
void f(SomeType s)
{
  if (s == null)
  {
    throw new ArgumentNullException("s cannot be null.");
  }

  // Use s
}

15
我严重怀疑一次简单的空值检查会对你的代码速度产生显著(甚至可以衡量)的影响。 - Heinzi
嗯,这取决于“Use s”做了什么。如果它只是“返回s.SomeField”,那么额外的检查可能会使该方法的速度慢上一个数量级。 - kaalus
12
@kaalus:"可能"?在我看来,"可能"并不能说服我。你的测试数据在哪里?而且,在第一时间改变会成为你的应用程序瓶颈的概率有多大? - Jon Skeet
@Jon:你说得对,我这里没有硬数据。如果你开发的是Windows Phone或Xbox,其中JIT内联将受到这个额外“if”的影响,并且分支非常昂贵,那么你可能会变得相当偏执。 - kaalus
4
@kaalus:所以在证明它能够产生显著影响之后,在那些特定的情况下调整你的代码风格,或者使用Debug.Assert(同样也只适用于这些情况,请参考Eric的回答),这样检查仅在某些构建中启用。 - Jon Skeet
为了增加第一位评论者的“严重怀疑” :-),典型的编译器优化是在数据流分析显示存在先前的空检查时消除隐式的空检查。因此,插入显式的空检查通常确实不会增加任何执行时间。即使不是这样,典型的现代超标量处理器也很可能能够在实际上零时间内执行检查,因为它可以在执行其他操作的同时并行执行检查。 - debater
10个回答

176

是的,有充分的理由:

  • 它确切地标识了哪里是 null,这可能不会在 NullReferenceException 中明显
  • 即使某些其他条件意味着该值未被取消引用,它也会在无效输入时使代码失败
  • 它使得异常发生在方法第一次取消引用之前,避免了方法在第一次取消引用之前执行其他任何副作用
  • 这意味着您可以确信如果将参数传递到其他东西中,您不会违反他们的合约
  • 它记录了您的方法要求(当然,使用Code Contracts更好)

现在针对你的异议:

  • 它会变慢:您是否真正发现过这是您代码的瓶颈?还是只是猜测?Nullity 检查非常快,并且在绝大多数情况下它们不会成为瓶颈。
  • 它使代码更难维护:我认为相反。我认为,当清楚地说明参数是否可以为空,并且你确信该条件已经得到执行时,使用代码更容易。

至于你的断言:

显然,使用 s 的代码会抛出异常。

真的吗?请考虑以下内容:

void f(SomeType s)
{
  // Use s
  Console.WriteLine("I've got a message of {0}", s);
}

代码使用了s,但它并不会抛出异常。如果s无效且表示有问题时,抛出异常是最合适的行为。

现在问题是在哪里加入这些参数验证检查。这是一个不同的问题。 你可能决定信任类中的所有代码,因此不需要在私有方法上费心。 你可能决定信任程序集的其余部分,所以不需要在内部方法上费心。 但你几乎肯定要验证公共方法的参数。

另外注意:ArgumentNullException的单参数构造函数重载应该只是参数名称,所以你的测试应该是:

if (s == null)
{
  throw new ArgumentNullException("s");
}

或者您可以创建一个扩展方法,从而允许使用较为简洁的方式:

s.ThrowIfNull("s");

在我的(通用)扩展方法中,如果原始值非空,我使其返回原始值,让您可以编写以下代码:

this.name = name.ThrowIfNull("name");

如果您不太在意参数名称,还可以创建一个不带参数名称的重载方法。

更新.NET 6

.NET API中有一个新方法,可以简化null检查语法。 此处提供了更多信息。

 ArgumentNullException.ThrowIfNull(someParameter);

8
赞成使用扩展方法。我做的事情完全相同,包括对ICollection进行ThrowIfEmpty等其他验证。 - Davy8
5
我认为它更难维护的原因是:与每个需要程序员介入的手动机制一样,这很容易出现错误、遗漏和代码混乱。不可靠的安全性比没有安全性更糟糕。 - kaalus
9
@kaalus:你对测试采取相同的态度吗?“我的测试无法捕获所有可能的错误,因此我不会编写任何测试”?当安全机制确实起作用时,它们将使问题更容易被发现,并减少在其他地方的影响(通过更早地捕获问题)。如果这种情况发生10次中有9次,那仍然比10次中没有一次要好... - Jon Skeet
2
@DoctorOreo:我不使用Debug.Assert。在生产环境中捕获错误(在它们破坏实际数据之前)比在开发过程中更重要。 - Jon Skeet
3
现在使用 C# 6.0,我们甚至可以使用 throw new ArgumentNullException(nameof(s)) - hrzafer
显示剩余17条评论

53

我同意Jon的观点,但我想补充一点。

我的态度关于何时添加显式的 null 检查基于以下前提:

  • 应该有一种方法使得单元测试能够运行程序中的每个语句。
  • throw 语句是 语句
  • if 的后果是一个 语句
  • 因此,应该有一种方法来执行 if (x == null) throw whatever; 中的 throw 语句。

如果没有 任何可能的方式 使该语句被执行,则无法进行测试,应该用 Debug.Assert(x != null); 来替换它。

如果有可能执行该语句,则编写该语句,然后编写一个单元测试来运行它。

对于公共类型的公共方法以这种方式检查其参数非常重要;您不知道用户将要做什么疯狂的事情。尽早给他们“嘿,你这个蠢蛋,你错了!” 异常。

相比之下,私有类型的私有方法更有可能处于控制参数并且可以有强有力保证参数永远不会为 null 的情况下;使用断言来记录不变量。


33

我现在已经使用这个东西一年了:

_ = s ?? throw new ArgumentNullException(nameof(s));

这是一行代码,并且舍弃 (_) 表示没有不必要的内存分配。


只能适用于字符串类型。对于 double 等类型无效。 - Cornelius J. van Dyk
@CorneliusJ.vanDyk 它适用于任何可能为空的内容。double 不能为 null,所以在那里当然没有意义。但它可以与 double? 一起使用。 - galdin
1
非常好的语法,但并不是原问题的答案。 - Nabeel


5
如果您需要更方便的方法来确保不会有空对象作为参数,可以看一下 Code Contracts

4
C# 11 预览版中,它非常酷!只需在参数名称末尾添加两个感叹号 !! 就能进行空参数检查。
void f(SomeType s!!)
{
    // Use s
}

这相当于:

void f(SomeType s)
{
    if (s is null)
    {
        throw new ArgumentNullException(nameof(s));
    }

    // Use s
}

一篇关于此的好的解释文章可以在这里找到。

它是否存在于当前的C# 11版本中? - Olivér Raisz
这实际上没有包含在最终发布版本中。 - undefined

3

当你遇到异常时,它可以帮助你省去一些调试时间。

ArgumentNullException 明确指出是 "s" 为空。

如果你没有进行检查并让代码崩溃,你会从该方法的某个未知行处得到一个 NullReferenceException。在发布版本中,你无法得到行号!


2
主要好处是从一开始就明确了您的方法要求。这使得其他开发人员清楚地知道,对于调用者向您的方法发送空值,这确实是一个错误。
检查还将在任何其他代码执行之前停止方法的执行。这意味着您不必担心方法所做的修改未完成。

0

原始代码:

void f(SomeType s)
{
  if (s == null)
  {
    throw new ArgumentNullException("s cannot be null.");
  }

  // Use s
}

将其改写为:

void f(SomeType s)
{
  if (s == null) throw new ArgumentNullException(nameof(s));
}

重新使用nameof的原因是它可以更容易地进行重构。如果您的变量s的名称发生了变化,那么调试消息也将被更新,而如果您只是硬编码变量的名称,那么随着时间的推移,它最终会过时。这是业界广泛采用的良好实践。

-2
int i = Age ?? 0;

所以针对您的示例:

if (age == null || age == 0)

或者:

if (age.GetValueOrDefault(0) == 0)

或者:

if ((age ?? 0) == 0)

或三元运算符:

int i = age.HasValue ? age.Value : 0;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接