我应该在私有/内部方法中添加null参数检查吗?

30

我正在编写一个包含多个公共类和方法的库,同时还有一些私有或内部类和方法是库自身使用。

在公共方法中,我进行了空值检查并使用如下方式进行异常抛出:

public int DoSomething(int number)
{
    if (number == null)
    {
        throw new ArgumentNullException(nameof(number));
    }
}

但是这让我开始思考,我应该在哪个层次上向方法添加参数 null 检查?我是否还应该开始将其添加到私有方法中?我只需要为公共方法这样做吗?


相关链接:https://softwareengineering.stackexchange.com/questions/100214/do-you-throw-an-argumentexception-or-argumentnullexception-from-private-methods - John Strood
7个回答

29

关于这个问题,目前尚没有统一的共识。所以,与其给出是或否的答案,我会尝试列举考虑因素:

  • 空值检查会使您的代码臃肿。如果您的过程简洁明了,那么在它们开头的空值保护可能占据了该过程的整体大小的大部分,而没有表达该过程的目的或行为。

  • 空值检查可以明确陈述先决条件。如果一个方法在其中一个值为空时会失败,那么在顶部放置一个空值检查是一种很好的方式,可以向轻松读者演示这一点,而不必寻找它被取消引用的位置。为了改进这一点,人们经常使用像Guard.AgainstNull这样的帮助器方法,而不是每次都编写检查。

  • 私有方法中的检查无法测试。通过在代码中引入一个分支,你无法完全遍历,这使得你无法完全测试该方法。这与测试记录类的行为并且该类代码存在的目的是提供该行为的观点相矛盾。

  • 让一个空值通过的严重程度取决于情况。通常,如果空值确实进入了方法,它将在几行后被取消引用,然后您将得到一个NullReferenceException。这真的没有什么比抛出一个ArgumentNullException更不清晰的东西了。另一方面,如果该引用在被取消引用之前多次传递,或者如果抛出NRE会使事情变得混乱,那么尽早抛出就更为重要。

  • 一些库(例如.NET的代码合同)允许进行一定程度的静态分析,可以为您的检查增加额外的好处。

  • 如果您正在与他人合作的项目上工作,可能已经存在团队或项目标准来涵盖此问题。


不要忘记抛出异常的性能影响。在制定这些标准时,这也应该是一个考虑因素。 - David T. Macknet
@DavidT.Macknet 这是真的。在我已经添加的点中(比如“不可测试”的那个点),我假设在这种情况下,null是真正的异常情况,在你所知道的代码路径中实际上不会遇到这种异常。使用守卫子句来控制私有方法或类似的任何事情,都是另一回事,它有自己的问题,其中包括性能问题。 - Ben Aaronson

11

如果你不是一个库开发者,在你的代码中不要过于防御

相反,写单元测试

事实上,即使你正在开发一个库,抛出异常大多数时候都是不好的。

1. 在c#中,不应该对int类型的null进行测试:

这会引发警告CS4072,因为它总是错误的。

2. 抛出异常意味着它是异常的:不正常和罕见的。

它不应该在生产代码中抛出。特别是因为异常堆栈遍历可能是一个占用CPU资源的任务。而且你永远不会确定异常将在哪里被捕获,如果它被捕获并记录或者只是简单地被忽略(在杀死其中一个后台线程之后),因为你无法控制用户代码。C#中没有像Java中的“checked exception”这样的机制,这意味着如果没有很好地记录,你永远不知道给定方法可能引发哪些异常。顺便说一下,这种文档必须与代码保持同步,这并不总是容易做到(增加维护成本)。
3. 异常会增加维护成本。
由于异常是在运行时和某些条件下抛出的,它们可能在开发过程中被检测得非常晚。正如你可能已经知道的那样,在开发过程中越晚发现错误,修复的代价就越高。我甚至见过异常抛出的代码进入了生产代码,并且在一周内没有抛出异常,然后每天都抛出异常(导致生产环境崩溃。糟糕!)。
4. 在无效输入上抛出异常意味着你无法控制输入。
这是关于库的公共方法的情况。然而,如果您可以使用另一种类型(例如非空类型,如int)在编译时检查它,那么这就是应该采取的方法。当然,由于它们是公共的,它们有责任检查输入。
想象一下使用他认为是有效数据的用户,然后由于副作用,堆栈跟踪中的一个方法抛出了 ArgumentNullException。
他会有什么反应?
他如何应对?
您能轻松提供解释信息吗?
5. 私有和内部方法绝不能抛出与其输入相关的异常。
您可以在代码中抛出异常,因为外部组件(可能是数据库、文件或其他)正在出现问题,您无法保证您的库将在其当前状态下继续正确运行。

将一个方法设置为public并不意味着它应该(仅仅是可以)从你的库之外被调用(请参考Martin Fowler的Public versus Published)。使用IOC、接口、工厂,只向用户发布必要的内容,同时让整个库类可用于单元测试。(或者您可以使用InternalsVisibleTo机制)。

6. 抛出没有任何解释信息的异常是在嘲笑用户

当一个工具坏了,却没有任何修复的线索,人们会有什么感受,无需提醒。是的,我知道。你来到SO并提出问题......

7. 无效的输入意味着代码出错

如果你的代码可以使用这个值产生有效的输出,那么它就不是无效的,你的代码应该管理它。添加一个单元测试来测试这个值。

8. 以用户为中心思考:

你喜欢使用的库抛出异常打破你的脸吗?像:“嘿,这是无效的,你应该知道!”

即使从您的角度来看 - 根据您对库内部的了解,输入无效,您如何向用户解释(友善和礼貌):

  • 清晰的文档(Xml文档和架构概述可能有所帮助)。
  • 发布库的xml文档。
  • 如果有异常,请提供清晰的错误说明。
  • 给予选择:

看看字典类,您更喜欢什么?您认为哪个调用速度最快?哪个调用可能会引发异常?

        Dictionary<string, string> dictionary = new Dictionary<string, string>();
        string res;
        dictionary.TryGetValue("key", out res);

或者

        var other = dictionary["key"];

9. 为什么不使用Code Contracts

这是一种避免丑陋的if then throw语句并将合同与实现隔离开来的优雅方式,允许同时为不同的实现重复使用合同。您甚至可以向库用户发布合同,以进一步解释如何使用该库。

总之,即使您可以轻松使用throw,即使您在使用.Net Framework时可能会遇到异常,也并不意味着可以毫无顾虑地使用它。


10

以下是我的观点:


一般情况

一般而言,在处理方法之前检查任何无效输入,以确保方法的稳健性,不管它是private、protected、internal、protected internal 或 public方法。虽然这种方法会付出一定的性能成本,但在大多数情况下,这是值得的,因为它能够避免更长的调试和修补代码的时间。


严格地说,但是...

严格来说,并不总是需要这样做。有些方法,通常是private的方法,可以无需进行任何输入检查,前提是您有完全的保证没有任何一个方法调用使用了无效的输入。对于这种情况,可能可以获得一些性能优势,特别是如果该方法被经常调用以执行一些基本计算/操作。对于这种情况,检查输入的有效性可能会显著降低性能。


公共方法

现在,public方法就比较棘手了。这是因为,严格来说,尽管访问修饰符单独可以说明谁可以使用这些方法,但它不能说明谁将会使用这些方法。此外,它还不能说明这些方法将以何种方式被使用(即这些方法是否会在给定的作用域内以无效输入被调用)。


最终决定因素

虽然代码中方法的访问修饰符可以提示如何使用方法,但最终使用方法的是人类,而如何使用它们以及使用什么输入取决于人类。因此,在某些罕见的情况下,即使有一个公共方法,它只在某些私有范围内调用,并且在该私有范围内,在调用公共方法之前为公共方法提供的输入是有效的。

在这种情况下,即使访问修饰符是public,也没有任何真正的需要检查无效的输入,除非出于健壮性设计原因。为什么呢?因为有人类完全知道何时以及如何调用这些方法!

我们可以看到,甚至公共方法也不能保证始终需要检查无效的输入。如果对于公共方法是这样的话,那么对于protected、internal、protected internal和private方法也是如此。


结论

因此,总之,我们可以说几件事情来帮助我们做决定:

  • 一般而言,出于健壮性设计方面的考虑,最好检查任何无效的输入,前提是不牺牲性能。这对于任何类型的访问修饰符都是正确的。
  • 如果可以显著提高性能,并且还可以保证调用方法的范围始终提供方法有效的输入,则可以跳过无效输入检查。
  • 通常我们跳过私有方法的此类检查,但不能保证不这样做公共方法。
  • 人类最终使用这些方法。无论访问修饰符如何提示使用方法,实际上如何使用和调用这些方法取决于编码人员。因此,我们只能谈论一般的/良好的实践方法,而不限制为唯一的做法。

  • 8
    1. 你的库的公共接口需要严格检查前提条件,因为你应该预期库的用户会犯错误并意外违反前提条件。帮助他们理解库中正在发生什么。

    2. 库中的私有方法不需要运行时检查,因为你自己调用它们。你完全掌控着传递什么。如果你想添加检查,因为你担心搞砸了,那就使用断言。它们将捕获你自己的错误,但不会在运行时影响性能。


    6
    尽管您标记了“语言无关”,但在我看来,可能没有一个“通用”的答案。
    值得注意的是,在您的示例中,您暗示了参数:因此,对于接受提示的语言,在进入函数之前它会触发一个错误,这意味着您不能采取任何措施。
    在这种情况下,唯一的解决方案是在调用函数之前检查参数...但由于您正在编写一个库,这是不可行的!
    另一方面,如果没有提示,仍然可以在函数内部进行检查。
    因此,在反思的这一步中,我已经建议放弃提示。
    现在让我们回到您的具体问题:应该检查到什么程度? 对于给定的数据片段,它只会发生在最高级别的地方,即它可以“进入”的地方(对于相同的数据可能有多个出现),因此从逻辑上讲,它只涉及公共方法。
    这是理论。但也许您计划使用一个庞大而复杂的库,所以很难确保所有“入口点”的准确性。
    在这种情况下,我建议相反:考虑到仅仅在任何地方应用您的控件,然后仅在您明确看到它是重复的地方省略它。
    希望这可以帮助到您。

    5

    在我看来,无论是私有方法还是公共方法,您都应该始终检查“无效”数据。

    从另一个角度来看...为什么您应该能够使用无效的东西,只因为该方法是私有的?这没有意义,对吧?始终尝试使用防御性编程,生活会更加愉快;-)


    2
    为什么你应该能够使用无效的东西,只因为方法是私有的?我不同意。由于私有方法仅从当前类调用,它们接收的数据来自此类,其中:1)最初它们是外部数据,通过非私有方法从外部传入,因此必须已经通过此方法进行了检查;2)它们由调用方法计算,该方法是我们谈论的库的一部分,它的责任是传递有效数据(即它属于库调试过程,而不是检查功能)。 - cFreed

    3
    这是一个偏好问题。但考虑的是,你为什么要检查null或者说检查有效输入。这可能是因为你希望让你的库的使用者知道他/她在错误地使用它。
    让我们想象一下,我们已经在一个库中实现了一个名为 PersonList 的类。这个列表只能包含Person 类型的对象。我们也已经在 PersonList 中实现了一些操作,因此我们不想它包含任何null值。
    考虑以下两种实现Add方法的方式: 实现1:
    public void Add(Person item)
    {
        if(_size == _items.Length)
        {
            EnsureCapacity(_size + 1);
        }
    
        _items[_size++] = item;
    }
    

    实现方式2

    public void Add(Person item)
    {
        if(item == null)
        {
            throw new ArgumentNullException("Cannot add null to PersonList");
        }
    
        if(_size == _items.Length)
        {
            EnsureCapacity(_size + 1);
        }
    
        _items[_size++] = item;
    }
    

    假设我们选择实施方案1

    • 现在可以在列表中添加空值
    • 所有应用于列表的操作都必须处理这些空值
    • 如果我们在运行操作时检查并抛出异常,当消费者调用其中一个操作时,他/她将收到关于异常的通知,并且此时他/她所做的错误是非常不清楚的(采取这种方法就没有任何意义)。

    如果我们选择实施方案2,我们将确保输入到我们的库中具有所需的质量,以便我们的类能够对其进行操作。这意味着我们只需要在此处处理它,然后我们就可以在实施其他操作时忘记它。

    当消费者在 .Add 上获得 ArgumentNullException 而不是在 .Sort 或相似位置时,他/她会更清楚地意识到自己正在错误地使用库。

    总之,我的首选是在由消费者提供并且不由库的私有/内部方法处理的参数时检查有效参数。这基本上意味着我们必须在具有参数的公共构造函数/方法中检查参数。我们的 private/internal 方法只能从我们的公共方法调用,并且它们已经检查了输入,这意味着我们可以放心使用!

    在验证输入时应考虑使用 Code Contracts


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