我应该在私有/内部方法中抛出空参数吗?
Posted
技术标签:
【中文标题】我应该在私有/内部方法中抛出空参数吗?【英文标题】:Should I throw on null parameters in private/internal methods? 【发布时间】:2016-04-22 09:39:36 【问题描述】:我正在编写一个库,其中包含几个公共类和方法,以及库本身使用的几个私有或内部类和方法。
在公共方法中,我有一个空检查和这样的抛出:
public int DoSomething(int number)
if (number == null)
throw new ArgumentNullException(nameof(number));
但这让我开始思考,我应该将参数空检查添加到方法的哪个级别?我是否也开始将它们添加到私有方法中?我应该只为公共方法这样做吗?
【问题讨论】:
相关:softwareengineering.stackexchange.com/questions/100214/… 【参考方案1】:如果您不是库开发人员,请不要在代码中采取防御措施
改为编写单元测试
事实上,即使你正在开发一个库,大部分时间都在扔:不好
1.绝对不能在 C# 中对 int
进行测试 null
:
它会引发警告 CS4072,因为它总是错误的。
2。抛出异常意味着它是异常的:异常和罕见。
它不应该在生产代码中引发。特别是因为异常堆栈跟踪遍历可能是一项 CPU 密集型任务。而且您永远无法确定异常会在哪里被捕获,如果它被捕获并记录,或者只是简单地被忽略(在杀死一个后台线程之后),因为您不控制用户代码。 c# 中没有 "checked exception"(就像在 java 中一样),这意味着您永远不知道 - 如果没有很好的文档记录 - 给定方法会引发什么异常。顺便说一句,这种文档必须与代码保持同步,这并不总是容易做到的(增加维护成本)。
3.例外情况会增加维护成本。
由于异常是在运行时和特定条件下引发的,它们可能会在开发过程的后期被检测到。您可能已经知道,在开发过程中越晚检测到错误,修复的成本就越高。我什至看到异常引发代码进入生产代码并且一周内没有引发,只是为了以后的每一天都引发(杀死生产。哎呀!)。
4.抛出无效输入意味着您无法控制输入。
图书馆的公共方法就是这种情况。但是,如果您可以在编译时使用另一种类型(例如不可为空的类型,如 int)检查它,那么这就是要走的路。当然,由于他们是公开的,他们有责任检查输入。
想象一下用户使用他认为有效的数据,然后由于副作用,堆栈跟踪深处的一个方法会抛出ArgumentNullException
。
5.私有方法和内部方法绝不应该抛出与其输入相关的异常。
您可能会在代码中抛出异常,因为外部组件(可能是数据库、文件或其他)行为异常,并且您无法保证您的库将继续在当前状态下正确运行。
公开一个方法并不意味着它应该(只是它可以)从你的库外部调用(Look at Public versus Published from Martin Fowler)。使用 IOC、接口、工厂并仅发布用户需要的内容,同时使整个库类可用于单元测试。 (或者你可以使用InternalsVisibleTo
机制)。
6.在没有任何解释信息的情况下抛出异常是在取笑用户
无需提醒工具损坏时的感受,也不知道如何修复它。是的,我知道。你来SO问一个问题...
7.无效输入意味着它会破坏您的代码
如果您的代码可以生成带有该值的有效输出,那么它不是无效的,您的代码应该管理它。添加一个单元测试来测试这个值。
8.从用户角度思考:
你喜欢你使用的库抛出异常来砸你的脸吗?比如:“嘿,无效,你应该知道的!”
即使从你的角度来看 - 根据你对图书馆内部的了解,输入是无效的,你如何向用户解释它(友善和礼貌):
清晰的文档(在 Xml 文档和架构摘要中可能会有所帮助)。 使用库发布 xml 文档。 清除异常中的错误说明(如果有)。 给出选择:i>看看 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】:这是一个偏好问题。但是请考虑为什么要检查 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
方法只能从我们的公共方法中调用,并且它们已经检查了输入,这意味着我们可以开始了!
在验证输入时也应考虑使用Code Contracts。
【讨论】:
【参考方案3】:您的库的公共接口值得严格检查前提条件,因为您应该预料到您的库的用户会犯错误并意外违反前提条件。帮助他们了解您的图书馆中正在发生的事情。
库中的私有方法不需要此类运行时检查,因为您自己调用它们。您可以完全控制所传递的内容。如果您因为害怕搞砸而想添加检查,请使用断言。它们会发现您自己的错误,但不会影响运行时的性能。
【讨论】:
【参考方案4】:以下是我的看法:
一般情况
一般来说,出于稳健性的原因,最好在方法中处理它们之前检查是否有任何无效输入 - 无论是private, protected, internal, protected internal, or public
方法。虽然这种方法会付出一些性能成本,但在大多数情况下,这样做是值得的,而不是花费更多时间来调试和稍后修补代码。
不过,严格来说...
然而,严格来说,并不总是需要这样做。一些方法,通常是private
的方法,可以不进行任何输入检查,前提是您有完整保证没有单个调用对于具有无效输入的方法。这可能会给您一些性能优势,尤其是当该方法被频繁调用以执行一些基本计算/操作时。对于这种情况,检查输入有效性可能会显着影响性能。
公共方法
现在public
方法更加棘手。这是因为,更严格地说,虽然访问修饰符单独可以告诉谁可以使用这些方法,但它不能告诉谁将 使用方法。此外,它也不能告诉 如何 方法将被使用(即,是否将在给定范围内使用无效输入调用方法)。
最终决定因素
虽然代码中方法的访问修饰符可以提示如何使用这些方法,但最终使用这些方法的是人类,这取决于人类如何使用它们以及输入什么。因此,在极少数情况下,可能有一个 public
方法,该方法仅在某些 private
范围内调用,并且在该 private
范围内,public
方法的输入保证在public
方法被调用。
在这种情况下,即使访问修饰符是public
,也没有任何真正需要检查无效输入,除了稳健设计原因。为什么会这样?因为有人类完全知道何时以及如何调用这些方法!
在这里我们可以看到,也不能保证public
方法总是需要检查无效输入。如果public
方法是这样,那么protected, internal, protected internal, and private
方法也一定是这样。
结论
所以,总而言之,我们可以说几句话来帮助我们做出决定:
一般情况下,出于稳健的设计原因,最好检查任何无效输入,前提是性能不受影响。这适用于任何类型的访问修饰符。 如果这样做可以显着提高性能增益,则可以跳过无效输入检查,前提是还可以保证调用方法的范围始终为方法提供有效输入.private
方法通常是我们跳过此类检查的地方,但不能保证 public
方法也不能这样做
人类是最终使用这些方法的人。不管访问修饰符如何暗示方法的使用,方法的实际使用和调用方式取决于编码器。因此,我们只能说一般/良好实践,而不是将其限制为唯一的方法。
【讨论】:
【参考方案5】:最终,对此没有统一的共识。因此,我不会给出是或否的答案,而是尝试列出做出此决定的考虑因素:
Null 检查会使您的代码膨胀。如果您的程序简洁,那么它们开头的 null 保护可能会构成整个程序大小的重要部分,而不会表达该程序的目的或行为。
Null 检查明确说明了一个前提条件。如果当其中一个值为 null 时方法将失败,则在顶部进行 null 检查是向普通读者展示这一点的好方法,而他们不必寻找取消引用的位置。为了改善这一点,人们经常使用名称为Guard.AgainstNull
的辅助方法,而不必每次都写检查。
私有方法中的检查是不可测试的。通过在您的代码中引入一个您无法完全遍历的分支,您就无法完全测试该方法。这与测试记录类的行为以及该类的代码存在以提供该行为的观点相冲突。
让空值通过的严重程度取决于具体情况。通常,如果一个 null 确实 进入该方法,它会在几行之后被取消引用,你会得到一个NullReferenceException
。这确实比抛出ArgumentNullException
清楚得多。另一方面,如果该引用在被取消引用之前被传递了相当长的时间,或者如果抛出 NRE 会使事情处于混乱状态,那么尽早抛出更为重要。
一些库,如 .NET 的代码契约,允许一定程度的静态分析,这可以为您的检查增加额外的好处。
如果您正在与其他人一起开展项目,则可能存在涵盖此内容的现有团队或项目标准。
【讨论】:
我们不要忘记抛出异常对性能的影响。在制定这些标准时,这也应该是一个考虑因素。 @DavidT.Macknet 是的。在我已经添加的点(例如“不可测试”的点)中,我假设 null 在这种情况下确实是异常的,在这种情况下,您知道它实际上没有代码路径会遇到该异常。用于私有方法或任何类似方法中的控制流的保护子句完全是另一回事,有其自身的问题,性能就是其中之一。【参考方案6】:在我看来,您应该始终检查“无效”数据——无论它是私有方法还是公共方法。
从另一个角度看...为什么您可以仅仅因为方法是私有的就可以处理无效的东西?没有意义,对吧?总是尝试使用防御性编程,你会在生活中更快乐;-)
【讨论】:
“为什么仅仅因为方法是私有的,你就可以处理无效的东西?”:我不同意。由于私有方法仅从当前类调用,因此它们传递的数据来自此类,其中:1)最初它们是外部数据,通过非私有方法来自外部,因此必须已经检查过这种方法; 2) 它们是由调用方法计算的,调用方法是我们所说的库的一部分,传输有效数据是该方法的职责(即属于库调试过程,而不是检查特性)。【参考方案7】:虽然您标记了language-agnostic
,但在我看来它可能不存在一般响应。
值得注意的是,在您的示例中,您暗示了参数:因此,对于接受提示的语言,它会在您进入函数后立即触发错误,然后您才能采取任何行动。 在这种情况下,唯一的解决方案是在调用您的函数之前检查参数...但是由于您正在编写一个库,这没有任何意义!
另一方面,在没有提示的情况下,检查函数内部仍然是现实的。 所以在这一步的反思中,我已经建议放弃暗示了。
现在让我们回到您的确切问题:应该检查到什么级别? 对于给定的数据片段,它只会发生在它可以“输入”的***别(同一数据可能会多次出现),因此从逻辑上讲,它只涉及公共方法。
这是理论。但是,也许您计划了一个庞大、复杂的库,因此要确保确定注册所有“入口点”可能并不容易。 在这种情况下,我建议相反:考虑仅在任何地方应用您的控件,然后仅在您清楚地看到它重复的地方省略它。
希望这会有所帮助。
【讨论】:
以上是关于我应该在私有/内部方法中抛出空参数吗?的主要内容,如果未能解决你的问题,请参考以下文章
Postgresql 在“id”列中抛出空值违反了 GenerationType.IDENTITY 的非空约束