C#中的空参数检查


84

在C#中,是否有任何充分的理由(除了更好的错误消息之外)向每个无效值都无效的函数添加参数无效检查?显然,使用s的代码仍然会引发异常。这样的检查会使代码变慢并且难以维护。

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

  // Use s
}

12
我严重怀疑简单的null检查会使您的代码变慢很多(甚至可衡量)。
Heinzi 2011年

好吧,这取决于“ Use s”的作用。如果它所做的只是“返回s.SomeField”,则额外的检查可能会将方法减慢一个数量级。
kaalus 2011年

10
@kaalus:可能吗?可能没有在我的书中删减。您的测试数据在哪里?首先,这种更改将成为您应用程序的瓶颈的可能性有多大?
乔恩·斯基特

@乔恩:你是对的,我这里没有硬数据。如果您为Windows Phone或Xbox开发,JIT内联将受此额外的“ if”影响,并且分支非常昂贵,则您可能会感到非常偏执。
kaalus

3
@kaalus:因此,在证明非常重要的情况下调整代码风格,或者在使用Debug.Assert(同样,仅在那些情况下,请参阅Eric的回答),以便在某些特定版本中进行检查。
乔恩·斯基特

Answers:


162

是的,有充分的理由:

  • 它可以准确地识别出什么是null,这从 NullReferenceException
  • 即使某些其他情况意味着该值未取消引用,这也会使代码在无效输入上失败
  • 它使异常发生该方法可能在第一次取消引用之前可能产生的任何其他副作用之前
  • 这意味着您可以确信,如果将参数传递给其他参数,则不会违反合同
  • 它记录了您方法的要求(当然,使用代码合同甚至更好)

现在,关于您的反对意见:

  • 速度较慢:您是否发现这实际上是代码中的瓶颈,还是在猜测?空位检查非常迅速,在大多数情况下,它们不会成为瓶颈
  • 它使代码更难维护:我认为相反。我认为使用代码更加容易,因为可以清楚地知道参数是否可以为null,并且可以确信该条件得到了执行。

对于您的主张:

显然,使用s的代码仍然会引发异常。

真?考虑:

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

使用s,但不会引发异常。如果将其s设为null无效,则表示出了点问题,这是最合适的行为。

现在将这些参数验证检查放在何处是另一回事。您可以决定信任自己类中的所有代码,因此不必费心使用私有方法。您可以决定信任其余的程序集,因此不必在意内部方法。您几乎应该肯定会验证公共方法的参数。

附带说明:单参数构造函数重载ArgumentNullException应仅是参数名称,因此您的测试应为:

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

或者,您可以创建一个扩展方法,允许稍微麻烦一些:

s.ThrowIfNull("s");

在我的(通用)扩展方法版本中,如果它不为null,则使它返回原始值,从而使您可以编写以下内容:

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

如果您不太担心该过载,那么也可以使用不带参数名称的重载。


8
+1为扩展方法。我已经做了同样的事情,包括其他验证,如ThrowIfEmptyICollection
Davy8

5
为什么我觉得更难维护:就像每一次都需要程序员干预的每一个手动机制一样,这容易出错和遗漏,并使代码混乱。片状安全总比没有安全差。
kaalus 2012年

8
@kaalus:您对测试使用相同的态度吗?“我的测试无法捕获所有可能的错误,因此我不会编写任何错误”?当安全机制的工作,他们会更容易发现问题和其他地区减少的影响(由早期捕捉问题)。如果这种情况发生10次中有9次,那仍然比发生10次中0次更好...
Jon Skeet 2012年

2
@DoctorOreo:我不使用Debug.Assert。与在开发中相比,发现生产中的错误(在错误破坏真实数据之前)更为重要。
乔恩·斯基特

2
现在使用C#6.0,我们甚至可以使用 throw new ArgumentNullException(nameof(s))
hrzafer 2015年

52

我同意乔恩的观点,但是我要补充一件事。

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

  • 您的单元测试应该有一种方法可以执行程序中的每个语句。
  • throw陈述是陈述
  • 的结果if是一个陈述
  • 因此,应该行使的方式throwif (x == null) throw whatever;

如果无法执行该语句,则无法对其进行测试,应将其替换为Debug.Assert(x != null);

如果有可能执行该语句,请编写该语句,然后编写执行该语句的单元测试。

这是特别是公共类型的公共方法,检查他们的论点以这种方式很重要; 您不知道用户将要做什么疯狂的事情。给他们“嘿,你这骨头,你做错了!” 尽快例外。

相比之下,私有类型的私有方法更可能出现在您控制参数的情况下,并且可以有力保证参数永远不会为null。使用断言来记录该不变性。


22

我已经使用了一年了:

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

这是一个单行,而throw(_)表示没有不必要的分配。


8

如果没有一个明确的if检查,它可以是很难搞清楚什么null,如果你没有自己的代码。

如果您NullReferenceException从一个没有源代码的库中深入了解,您很可能很难弄清楚您做错了什么。

这些if检查不会使您的代码明显变慢。


请注意,ArgumentNullException构造函数的参数是参数名称,而不是消息。
您的代码应为

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

我编写了一个代码片段来简化此操作:

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>Check for null arguments</Title>
            <Shortcut>tna</Shortcut>
            <Description>Code snippet for throw new ArgumentNullException</Description>
            <Author>SLaks</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
                <SnippetType>SurroundsWith</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>Parameter</ID>
                    <ToolTip>Paremeter to check for null</ToolTip>
                    <Default>value</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp"><![CDATA[if ($Parameter$ == null) throw new ArgumentNullException("$Parameter$");
        $end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>


2

主要好处是,您从一开始就明确了方法的要求。这使从事代码工作的其他开发人员可以清楚地知道,调用者向您的方法发送null值确实是错误的。

该检查还将在执行任何其他代码之前暂停该方法的执行。这意味着您不必担心该方法未完成的修改。


2

当您遇到该异常时,它可以节省一些调试时间。

ArgumentNullException明确指出是“ s”为空。

如果您没有进行检查,而是让代码吹嘘,那么您将从该方法中某个未标识的行中获取NullReferenceException。在发行版本中,您没有行号!


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更改,则调试消息也将更新,而如果您仅对变量名进行硬编码,则随着时间的推移,变量名最终将过时。这是工业上的好习惯。


如果您建议进行更改,请说明原因。还要确保您正在回答所问的问题。
CodeCaster

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.