我应该在私有/内部方法中抛出null参数吗?

时间:2016-01-17 01:17:10

标签: c# exception-handling language-agnostic software-design

我正在编写一个包含多个公共类和方法的库,以及库本身使用的几个私有或内部类和方法。

在公共方法中,我有一个空检查和这样的抛出:

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

但是这让我思考,我应该在什么级别添加参数null检查方法?我是否也开始将它们添加到私有方法?我应该只为公共方法做这件事吗?

7 个答案:

答案 0 :(得分:25)

最终,对此没有统一的共识。因此,我没有给出是或否答案,而是尝试列出做出此决定的注意事项:

  • Null检查膨胀您的代码。如果您的程序简明扼要,那么它们开头的空值守卫可能构成程序总体规模的重要部分,而不表达该程序的目的或行为。

  • 空检查表达了一个先决条件。如果一个方法在其中一个值为null时失败,那么在顶部进行空检查是一种很好的方式,可以向一个随意的读者证明这一点,而不必去寻找它被解除引用的地方。为了改善这种情况,人们经常使用名为Guard.AgainstNull的帮助方法,而不是每次都要写支票。

  • 检查私有方法是不可测试的。通过在代码中引入一个无法完全遍历的分支,您无法完全测试该方法。这与测试记录类的行为以及该类的代码存在以提供该行为的观点相冲突。

  • 让null通过的严重程度取决于具体情况。通常情况下,如果null 进入该方法,则稍后会将其取消引用几行,并且您将获得NullReferenceException。这真的不如抛出ArgumentNullException那么明确。另一方面,如果在被解除引用之前传递了相当多的引用,或者如果抛出NRE会使事情处于凌乱状态,那么提前投掷就更为重要了。

  • 某些库,如.NET的代码合同,允许一定程度的静态分析,这可以为您的支票增加额外的好处。

  • 如果您正在与他人合作开展项目,可能会有现有的团队或项目标准。

答案 1 :(得分:10)

如果您不是图书馆开发人员,请不要在代码中采取防御措施

改为编写单元测试

事实上,即使您正在开发图书馆,投掷也是大部分时间:BAD

<强> 1。在c#:

中永远不能在null上测试int

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

<强> 2。抛出异常意味着它是例外:异常和罕见。

永远不应该提高生产代码。特别是因为异常堆栈跟踪遍历可以是cpu密集型任务。而且你永远不会确定异常将被捕获的位置,是否被捕获和记录,或者只是默默地忽略(在杀死你的一个后台线程之后)因为你不控制用户代码。在c#中没有“已检查异常”(如在java中),这意味着您永远不会知道 - 如果没有详细记录 - 给定方法可能引发的异常。顺便说一句,这种文档必须与代码保持同步,这并不总是容易做到(增加维护成本)。

第3。例外会增加维护成本。

由于在运行时抛出异常并且在某些条件下,可以在开发过程的后期检测到异常。您可能已经知道,在开发过程中检测到的错误越晚,修复的成本就越高。我甚至看到异常提升代码进入生产代码并且不会提高一周,只是为了以后每天提高(杀死生产。哎呀!)。

<强> 4。投掷无效输入意味着您无法控制输入。

图书馆的公共方法就是这种情况。但是,如果您可以在编译时使用其他类型(例如像int这样的非可空类型)检查它,那么它就是要走的路。当然,由于它们是公开的,因此检查输入是他们的责任。

想象一下,如果用户使用他认为有效的数据,然后通过副作用,堆栈跟踪深处的方法会追踪ArgumentNullException

  • 他的反应是什么?
  • 他怎么能应付这个?
  • 您是否可以轻松提供解释信息?

<强> 5。私有和内部方法永远不应该抛出与其输入相关的异常。

您可能会在代码中抛出异常,因为外部组件(可能是数据库,文件或其他)行为不正常,您无法保证您的库将继续在其当前状态下正常运行。

将方法公开并不意味着它应该(只能它)可以从库外部调用(Look at Public versus Published from Martin Fowler)。使用IOC,接口,工厂并仅发布用户需要的内容,同时使整个库类可用于单元测试。 (或者您可以使用InternalsVisibleTo机制)。

<强> 6。在没有任何解释信息的情况下抛出异常就是取笑用户

无需提醒工具破损时可以有什么样的感受,而不知道如何解决它。是的我知道。你来SO并提出问题......

<强> 7。输入无效意味着它会破坏您的代码

如果您的代码可以生成带有该值的有效输出,则它不是无效的,您的代码应该对其进行管理。添加单元测试以测试此值。

<强> 8。用用户术语来思考:

你喜欢当你使用的图书馆抛出异常粉碎你的脸时,你喜欢吗?就像:“嘿,这是无效的,你应该知道的!”

即使从你的角度来看 - 了解你的库内部,输入也是无效的,你如何向用户解释它(善良和礼貌):

  • 清除文档(在Xml文档和架构摘要中可能有所帮助)。
  • 使用库发布xml doc。
  • 清除异常中的错误说明(如果有)。
  • 选择:

看看Dictionary类,你更喜欢什么?你觉得哪个电话最快?什么电话会引发异常?

        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时遇到异常提升,也不会意味着它可以在没有小心。

答案 2 :(得分:9)

以下是我的意见:

一般情况

一般来说,最好在健壮性原因的方法中处理它们之前检查任何无效输入 - 是private, protected, internal, protected internal, or public方法。虽然为此方法支付了一些性能成本,但在大多数情况下,这是值得做的,而不是支付更多时间进行调试并稍后修补代码。

严格说来,但是......

然而,严格地说,并不总是需要这样做。某些方法(通常是private个)可以在没有任何输入检查的情况下保留,只要您完整保证没有单< / em>使用无效输入调用方法。这可能会为您带来一些性能优势,特别是如果方法经常调用以执行一些基本的计算/操作。对于这种情况,检查输入有效性可能会显着影响性能

公共方法

现在public方法比较棘手。这是因为,更严格来说,虽然访问修饰符单独可以告诉谁可以使用这些方法,但无法告诉谁使用这些方法。更重要的是,它也无法告诉如何使用方法(即,是否将在给定范围内使用无效输入调用方法)。

最终决定因素

尽管代码中方法的访问修饰符可以提示关于如何使用这些方法,但最终, human 将使用这些方法,并且它取决于人类如何他们将使用它们和什么输入。因此,在极少数情况下,可以使用仅在public范围和private范围内调用的private方法,public的输入在调用public方法之前,方法保证有效。

在这种情况下,即使是访问修饰符public,也没有任何真实需要检查无效输入, robust 设计理由。为什么会这样呢?因为完全知道如何时应该调用这些方法!

我们可以看到,无法保证public方法总是要求检查无效输入。如果public方法也是如此,那么protected, internal, protected internal, and private方法也必须如此。

结论

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

  • 通常,如果性能不受影响,最好是出于稳健的设计原因检查任何无效输入。对于任何类型的访问修饰符都是如此。
  • 如果可以通过这样做显着提高性能增益,则可以跳过无效输入检查,前提是还可以保证调用方法的范围始终为方法提供有效输入
  • private方法通常是我们跳过此类检查的地方,但不能保证我们也不能为public方法执行此操作
  • 人类是最终使用这些方法的人。无论访问修饰符如何暗示方法的使用,实际使用和调用方法的方式取决于编码器。因此,我们只能说一般/良好做法,而不是限制它这样做的唯一方式。

答案 3 :(得分:8)

  1. 您图书馆的公共界面应该严格检查前提条件,因为您应该期望图书馆的用户出错并且意外违反前提条件。帮助他们了解您图书馆的动态。

  2. 库中的私有方法不需要这样的运行时检查,因为您自己调用它们。你完全可以控制你的传球。如果你想添加检查,因为你害怕搞砸,那么使用断言。他们会抓住你自己的错误,但不会妨碍运行期间的性能。

答案 4 :(得分:6)

虽然您标记了language-agnostic,但在我看来,它可能不存在常规响应。

值得注意的是,在你的例子中,你暗示了这个论点:所以在接受提示的语言中,一旦进入该功能,它就会发出错误,然后才能采取任何行动。
在这种情况下,唯一的解决方案是在调用函数之前检查参数 ...但是因为你正在编写一个库,这是没有意义的!

另一方面,没有提示,检查功能内部仍然是现实的 所以在反思的这一步,我已经建议放弃暗示。

现在让我们回到您的确切问题:应该检查什么级别? 对于给定的数据片段,它只发生在它可以“进入”的最高级别(可能是同一数据的几次出现),所以逻辑上它只涉及公共方法。

这就是理论。但也许你计划一个庞大而复杂的图书馆,因此要确保注册所有“入口点”可能并不容易。 在这种情况下,我建议相反:考虑只是在任何地方应用你的控件,然后只在你清楚地看到它重复的地方省略它。

希望这有帮助。

答案 5 :(得分:5)

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

从另一个方面来看......为什么你应该能够处理一些无效的东西,因为这个方法是私有的?没有意义,对吧?总是尝试使用防御性编程,你会更幸福生活; - )

答案 6 :(得分:3)

这是一个偏好问题。但请考虑为什么要检查null或者检查有效输入。这可能是因为你想让你的图书馆的消费者知道他/她错误地使用它。

让我们假设我们在库中实现了一个类PersonList。此列表只能包含Person类型的对象。我们还在PersonList上实现了一些操作,因此我们不希望它包含任何空值。

考虑此列表的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

  • 现在可以在列表中添加空值
  • 列表中实施的所有操作都必须处理theese null值
  • 如果我们应该在我们的操作中检查并抛出异常,当他/她正在调用其中一个操作时,将通知消费者该异常,并且在此状态下将非常不清楚他/她做错了什么(采用这种方法是没有任何意义的。)

如果我们选择使用实现2,我们确保对我们的库的输入具有我们的类对其进行操作所需的质量。这意味着我们只需要处理这个问题,然后在实施其他操作时我们可以忘记它。

当消费者在ArgumentNullException而不是.Add.Sort获得privateinternal时,他/她以错误的方式使用图书馆也会变得更加清晰。 similair。

总结一下,我的偏好是在消费者提供它时检查有效参数,而不是由库的私有/内部方法处理。这基本上意味着我们必须检查public的构造函数/方法中的参数并获取参数。我们的app.filters / angular.module("MySecondModule", [app.filters]) 方法只能从公共方法中调用,并且已经检查了输入,这意味着我们很高兴!

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