use*_*993 29 c# language-agnostic exception-handling software-design
我正在编写一个包含多个公共类和方法的库,以及库本身使用的几个私有或内部类和方法.
在公共方法中,我有一个空检查和抛出这样的:
public int DoSomething(int number)
{
if (number == null)
{
throw new ArgumentNullException(nameof(number));
}
}
Run Code Online (Sandbox Code Playgroud)
但是这让我思考,我应该在什么级别添加参数null检查方法?我是否也开始将它们添加到私有方法?我应该只为公共方法做吗?
Ben*_*son 25
最终,对此没有统一的共识.因此,我将尝试列出做出此决定的注意事项,而不是给出是或否答案:
空检查会使代码膨胀.如果您的程序简明扼要,那么它们开头的空值保护可能构成程序整体规模的重要部分,而不表达该程序的目的或行为.
空检查表达了一个先决条件.如果一个方法在其中一个值为null时失败,那么在顶部进行空检查是一种很好的方法,可以向一个随意的读者证明这一点,而不必去寻找它被解除引用的地方.为了改善这种情况,人们经常使用带有名称的帮助方法Guard.AgainstNull,而不是每次都要写支票.
检查私有方法是不可测试的.通过在代码中引入一个无法完全遍历的分支,您无法完全测试该方法.这与测试记录类的行为以及该类的代码存在以提供该行为的观点相冲突.
让null通过的严重程度取决于具体情况.通常情况下,如果空不进入的方法,这将是一个几行后提领,你会得到一个NullReferenceException.这真的没有什么比投掷更清楚ArgumentNullException.另一方面,如果该引用在被取消引用之前传递了很多,或者如果抛出NRE会使事情处于混乱状态,那么提前投掷就更为重要了.
某些库(如.NET的代码契约)允许一定程度的静态分析,这可以为您的检查增加额外的好处.
如果您正在与其他人一起开展项目,可能会有现有的团队或项目标准.
Fab*_*Fab 10
事实上,即使你正在开发一个图书馆,投掷也是大部分时间:不好
1.测试null上int绝不能做在C#中:
它引发了警告CS4072,因为它总是错误的.
2.抛出异常意味着它是例外:异常和罕见.
它永远不应该提高生产代码.特别是因为异常堆栈跟踪遍历可以是cpu密集型任务.而且你永远不会确定异常将被捕获的位置,是否被捕获和记录,或者只是默默地忽略(在杀死你的一个后台线程之后)因为你不控制用户代码.c#中没有"已检查的异常"(就像在java中一样),这意味着你永远不知道 - 如果没有详细记录 - 给定方法可以引发什么异常.顺便说一句,这种文档必须与代码保持同步,这并不总是容易做到(增加维护成本).
3.例外会增加维护成本.
由于在运行时和某些条件下抛出异常,因此可以在开发过程的后期检测到异常.您可能已经知道,在开发过程中检测到的错误越晚,修复的成本就越高.我甚至看到异常提升代码进入生产代码而不是提前一周,只是为了以后每天提高(杀死生产.哎呀!).
4.抛出无效输入意味着您无法控制输入.
公共图书馆方法就是这种情况.但是,如果您可以在编译时使用其他类型(例如像int这样的非可空类型)检查它,那么它就是要走的路.当然,由于它们是公开的,因此检查输入是他们的责任.
想象一下,使用他认为是有效数据的用户,然后通过副作用,堆栈跟踪深处的方法进入a ArgumentNullException.
5.私有和内部方法永远不应该抛出与其输入相关的异常.
您可能会在代码中抛出异常,因为外部组件(可能是数据库,文件或其他)行为不正常,您无法保证您的库将继续在其当前状态下正常运行.
将方法公之于众并不意味着它应该(只有它可以)从你的图书馆外部调用(看看公共与发布的马丁福勒).使用IOC,接口,工厂并仅发布用户需要的内容,同时使整个库类可用于单元测试.(或者您可以使用该InternalsVisibleTo机制).
6.在没有任何解释信息的情况下抛出异常就是取笑用户
无需提醒工具被破坏时可以有什么样的感受,而不需要知道如何修复它.是的我知道.你来到SO并问一个问题......
7.输入无效意味着它会破坏您的代码
如果您的代码可以生成带有该值的有效输出,则它不是无效的,您的代码应该对其进行管理.添加单元测试以测试此值.
8.用用户术语思考:
你喜欢当你使用的图书馆抛出异常粉碎你的脸时,你喜欢吗?喜欢:"嘿,这是无效的,你应该知道的!"
即使从您的角度来看 - 根据您对库内部的了解,输入无效,您如何向用户解释(善良和礼貌):
看看Dictionary类,你更喜欢什么?你觉得哪个电话最快?什么电话会引发异常?
Dictionary<string, string> dictionary = new Dictionary<string, string>();
string res;
dictionary.TryGetValue("key", out res);
Run Code Online (Sandbox Code Playgroud)
要么
var other = dictionary["key"];
Run Code Online (Sandbox Code Playgroud)
9.为什么不使用代码合同?
这是避免丑陋if then throw并将合同与实现隔离开的优雅方式,允许同时重用不同实现的合同.您甚至可以将合同发布给您的库用户,以进一步向他解释如何使用该库.
总而言之,即使您可以轻松使用throw,即使您在使用.Net Framework时可能会遇到异常提升,但这并不意味着可以毫不谨慎地使用它.
以下是我的意见:
一般来说,最好在出于稳健性原因的方法中处理它们之前检查任何无效输入 - 无论是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方法做到这一点您的图书馆的公共界面值得仔细检查前提条件,因为您应该期望图书馆的用户出错并违反前提条件.帮助他们了解您图书馆的动态.
库中的私有方法不需要这样的运行时检查,因为您自己调用它们.你完全可以控制你的传球.如果你想添加检查,因为你害怕搞砸,那么使用断言.它们会捕获您自己的错误,但不会妨碍运行时的性能.
虽然你标记了language-agnostic,但在我看来它可能不存在一般反应.
值得注意的是,在你的例子中,你暗示了这个论点:所以当一个语言接受暗示时,它会在你进行任何动作之前立即触发错误.
在这种情况下,唯一的解决方案是在调用函数之前检查参数...但是因为你正在编写一个库,所以没有意义!
另一方面,没有提示,检查功能内部仍然是现实的.
所以在反思的这一步,我已经建议放弃暗示.
现在让我们回到您的确切问题:应该检查什么级别?对于给定的数据片段,它只发生在它可以"进入"的最高级别(可能是相同数据的几次出现),所以逻辑上它只涉及公共方法.
那就是理论.但也许你计划一个庞大而复杂的图书馆,因此要确保注册所有"入口点"可能并不容易.
在这种情况下,我建议相反:考虑只是在任何地方应用你的控件,然后只在你清楚地看到它重复的地方省略它.
希望这可以帮助.
在我看来,你应该总是检查"无效"数据 - 无论是私人还是公共方法.
从另一个方面来看......为什么你应该能够处理一些无效的东西,因为这个方法是私有的?没有意义,对吧?总是尝试使用防御性编程,你会在生活中更快乐;-)
| 归档时间: |
|
| 查看次数: |
2550 次 |
| 最近记录: |