我应该在私有/内部方法中抛出空参数吗?
本文关键字:参数 方法 内部 我应该 | 更新日期: 2023-09-27 18:30:37
我正在编写一个库,其中包含多个公共类和方法,以及库本身使用的几个私有或内部类和方法。
在公共方法中,我有一个空检查和一个像这样的抛出:
public int DoSomething(int number)
{
if (number == null)
{
throw new ArgumentNullException(nameof(number));
}
}
但随后这让我想到,我应该将参数 null 检查添加到方法的什么级别?我是否也开始将它们添加到私有方法中?我应该只为公共方法执行此操作吗?
最终,对此没有统一的共识。因此,我将尝试列出做出此决定的注意事项,而不是给出是或否的答案:
-
空检查会使代码膨胀。如果过程简洁,则过程开头的 null 保护可能构成过程整体大小的重要部分,而不表示该过程的目的或行为。
-
空检查明确地陈述了前提条件。如果当其中一个值为 null 时方法将失败,则在顶部进行 null 检查是向普通读者演示这一点的好方法,而无需他们寻找取消引用的位置。为了改善这一点,人们经常使用名称为
Guard.AgainstNull
的辅助方法,而不必每次都编写检查。 -
私有方法中的检查是不可测试的。通过在代码中引入无法完全遍历的分支,无法完全测试该方法。这与以下观点相冲突:测试记录了类的行为,并且该类的代码存在以提供该行为。
-
让 null 通过的严重性取决于情况。通常,如果 null 确实进入该方法,它将在几行后被取消引用,并且您将获得
NullReferenceException
。这真的不比扔ArgumentNullException
更清楚.另一方面,如果该引用在取消引用之前传递了很多,或者如果抛出 NRE 会让事情处于混乱状态,那么尽早抛出就更重要了。 -
一些库,如 .NET的代码合约允许一定程度的静态分析,这可以为您的检查增加额外的好处。
-
如果您正在与其他人一起处理一个项目,则可能有涵盖此内容的现有团队或项目标准。
如果你不是库开发人员,不要在你的代码中防御
改为编写单元测试
事实上,即使你正在开发一个库,大多数时候都是抛出:坏
1. 对int
的测试null
绝不能在 C# 中完成:
它会引发警告 CS4072,因为它始终是假的。
2. 抛出异常意味着它是异常的:异常和罕见。
它永远不应该在生产代码中引发。特别是因为异常堆栈跟踪遍历可能是 CPU 密集型任务。而且你永远无法确定异常会被捕获到哪里,如果它被捕获并记录下来,或者只是简单地默默忽略(在杀死你的一个后台线程之后),因为你不控制用户代码。在 c# 中没有"检查异常"(如在 java 中),这意味着你永远不知道 - 如果没有很好的文档 - 给定的方法可能会引发哪些异常。顺便说一下,这种文档必须与代码保持同步,这并不总是容易做到的(增加维护成本)。
3. 异常会增加维护成本。
由于异常是在运行时和某些条件下引发的,因此可以在开发过程的后期检测到它们。您可能已经知道,在开发过程中检测到错误的时间越晚,修复成本就越高。我什至看到异常提高代码进入生产代码,并且一周不提高,只是为了以后每天都提高(杀死生产。
4. 抛出无效输入意味着您无法控制输入。
图书馆的公共方法就是这种情况。但是,如果您可以在编译时使用另一种类型(例如像 int 这样的不可为空的类型)检查它,那么这就是要走的路。当然,由于他们是公开的,他们有责任检查输入。
想象一下,用户使用他认为是有效数据,然后通过副作用,堆栈跟踪深处的方法ArgumentNullException
。
- 他会有什么反应?
- 他怎么能应付呢?
- 您提供解释消息是否容易?
5. 私有和内部方法永远不应引发与其输入相关的异常。
您可能会在代码中引发异常,因为外部组件(可能是数据库、文件或其他组件)行为异常,并且您无法保证库在其当前状态下继续正常运行。
公开方法并不意味着它应该(只是可以)从库外部调用(查看 Public 与 Martin Fowler 的发布)。使用 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. 为什么不使用代码合约?
这是一种避免丑陋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
方法也这样做 - 人类是最终使用这些方法的人。无论访问修饰符如何暗示方法的使用,实际使用和调用方法的方式取决于编码器。因此,我们只能说一般/良好做法,而不限制它是这样做的唯一方法。
-
库的公共接口需要严格检查前提条件,因为您应该预料到库的用户会犯错误并意外违反前提条件。帮助他们了解您的图书馆中发生了什么。
-
库中的私有方法不需要此类运行时检查,因为您可以自己调用它们。您可以完全控制要传递的内容。如果你想添加检查,因为你害怕搞砸,那么使用断言。它们会捕获您自己的错误,但不会妨碍运行时的性能。
虽然你标记了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
- 现在可以在列表中添加空值
- 列表中实现的所有操作都必须处理这些空值
- 如果我们应该在操作中检查并抛出异常,则当消费者调用其中一个操作时,他/她将收到有关异常的通知,并且在此状态下将非常不清楚他/她做错了什么(只是采用这种方法没有任何意义)。
如果我们选择使用实现 2,我们确保对库的输入具有类对其进行操作所需的质量。这意味着我们只需要在这里处理这个问题,然后我们可以在实现其他操作时忘记它。
对于消费者来说,当他/她获得.Add
而不是.Sort
或类似物的ArgumentNullException
时,他/她以错误的方式使用库也会变得更加清楚。
总而言之,我的偏好是在消费者提供参数并且未由库的私有/内部方法处理时检查有效参数。这基本上意味着我们必须检查公共构造函数/方法中的参数并采用参数。我们的private
/internal
方法只能从我们的公共方法调用,并且它们已经准备好检查了输入,这意味着我们很好!
验证输入时,还应考虑使用代码协定。