繁体   English   中英

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

[英]Should I throw on null parameters in private/internal methods?

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

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

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

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

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

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

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

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

  • 让null通过的严重程度取决于具体情况。 通常情况下,如果空进入的方法,这将是一个几行后提领,你会得到一个NullReferenceException 这实际上并不比抛出ArgumentNullException更清楚。 另一方面,如果该引用在被取消引用之前传递了很多,或者如果抛出NRE会使事情处于混乱状态,那么提前投掷就更为重要了。

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

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

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

改为编写单元测试

事实上,即使你正在开发一个图书馆,投掷也是大部分时间:不好

1.在int上测试null绝对不能在c#中完成

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

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

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

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

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

4.抛出无效输入意味着您无法控制输入

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

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

  • 他的反应是什么?
  • 他怎么能应付这个?
  • 您是否容易提供解释信息?

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

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

将方法公之于众并不意味着它应该(只有它可以)从你的库外部调用( 看看公共与发布的Martin Fowler )。 使用IOC,接口,工厂并仅发布用户需要的内容,同时使整个库类可用于单元测试。 (或者您可以使用InternalsVisibleTo机制)。

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

无需提醒工具被破坏时可以有什么样的感受,而不需要知道如何修复它。 是的我知道。 你来到SO并问一个问题......

7.输入无效意味着它会破坏您的代码

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

8.用用户术语思考:

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

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

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

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

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

要么

        var other = dictionary["key"];

9.为什么不使用代码合同

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

总而言之,即使您可以轻松使用throw ,即使您在使用.Net Framework时可能会遇到异常提升,但这并不意味着可以毫不谨慎地使用它。

以下是我的意见:


一般情况

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


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

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


公共方法

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


终极决定因素

虽然代码中方法的访问修饰符可以提示如何使用这些方法,但最终, 人类将使用这些方法,并且由人们决定如何使用它们以及使用什么输入。 因此,在极少数情况下,可以使用仅在某个private范围内调用的public方法,并且在该private范围内, public方法的输入在调用public方法之前保证有效。

在这种情况下,即使访问修饰符是public ,除了稳健的设计原因外,没有任何实际需要检查无效输入。 为什么会这样呢? 因为有些完全知道何时以及如何调用这些方法!

在这里我们可以看到,不能保证public方法总是需要检查无效输入。 如果对于public方法也是如此,那么对于protected, internal, protected internal, and private方法也必须如此。


结论

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

  • 通常 ,如果性能不受影响 ,最好对稳健的设计原因检查任何无效输入。 对于任何类型的访问修饰符都是如此。
  • 如果通过这样做可以显着提高性能增益,则可以跳过无效输入检查,前提是还可以保证调用方法的范围始终为方法提供有效输入。
  • private方法通常是我们跳过这种检查的地方,但不能保证我们也不能为public方法这样做
  • 人类是最终使用这些方法的人。 无论访问修饰符如何暗示方法的使用,实际使用和调用方法的方式取决于编码器。 因此,我们只能说一般/良好实践,而不是限制它是唯一的方法
  1. 您的图书馆的公共界面值得仔细检查前提条件,因为您应该期望图书馆的用户犯错误并违反前提条件。 帮助他们了解您图书馆的动态。

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

虽然你标记了language-agnostic ,但在我看来它可能不存在一般的反应。

值得注意的是,在你的例子中,你暗示了这个论点:所以当一个语言接受暗示时,它会在你进行任何动作之前立即触发错误。
在这种情况下,唯一的解决方案是在调用函数之前检查参数...但是因为你正在编写一个库,所以没有意义!

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

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

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

希望这可以帮助。

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

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

这是一个偏好问题。 但请考虑为什么要检查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,我们确保对我们的库的输入具有我们的类对其进行操作所需的质量。 这意味着我们只需要处理这个问题,然后在实施其他操作时我们可以忘记它。

当消费者在.Add而不是.Sort或similair上获得ArgumentNullException时,他/她以错误的方式使用库也将变得更加清楚。

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

验证输入时还应考虑使用代码合同

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM