昨天我和同事讨论了什么是首选的错误报告方法.主要是我们讨论了异常或错误代码的使用,以报告应用程序层或模块之间的错误.
您使用什么规则来决定是否抛出异常或返回错误代码以进行错误报告?
在高级别的东西,例外; 在低级别的东西,错误代码.
异常的默认行为是展开堆栈并停止程序,如果我正在编写脚本并且我找到一个不在字典中的密钥它可能是一个错误,我希望程序停止让我知道这一切.
但是,如果我正在编写一段代码,我必须知道在每种可能情况下的行为,那么我想要错误代码.否则,我必须知道我的函数中的每一行都可以抛出的每个异常,以了解它将做什么(阅读使航空公司接地的例外情况,以了解这是多么棘手).编写对每种情况都适当做出反应的代码(包括不愉快的代码)是很繁琐和困难的,但那是因为编写无错误的代码既乏味又困难,不是因为你传递了错误代码.
无论雷蒙德陈 和 乔尔都反对使用异常的一切做了一些雄辩的论点.
我通常更喜欢异常,因为它们有更多的上下文信息,并且能够以更清晰的方式向程序员传达(如果使用得当)错误.
另一方面,错误代码比异常更轻量级但更难维护.错误检查可能会无意中被省略.错误代码更难维护,因为您必须保留包含所有错误代码的目录,然后打开结果以查看引发的错误.错误范围在这里可能有所帮助,因为如果我们唯一感兴趣的是如果我们是否存在错误,则检查更简单(例如,大于或等于0的HRESULT错误代码是成功的,小于零就是失败).它们可能会被无意中省略,因为没有编程强制开发人员会检查错误代码.另一方面,您不能忽略异常.
总而言之,我几乎在所有情况下都偏好错误代码.
我更喜欢例外,因为
他们中断了逻辑的流动
它们受益于类层次结构,它提供了更多特性/功能
正确使用时可以表示各种错误(例如,InvalidMethodCallException也是一个LogicException,因为当代码中存在应该在运行时检测到的错误时,会发生这两种错误,并且
它们可用于增强错误(即FileReadException类定义可以包含代码以检查文件是否存在,或者是否已锁定等)
您的函数的调用者可以忽略错误代码(通常是!).例外至少迫使他们以某种方式处理错误.即使他们处理它的版本是一个空的捕获处理程序(叹息).
错误代码的例外,毫无疑问.与错误代码一样,您可以获得与异常相同的好处,而且还有更多错误代码的缺点.唯一的例外是它的开销略高; 但是在这个时代,对于几乎所有应用来说,这个开销应该被认为是微不足道的.
以下是一些讨论,比较和对比这两种技术的文章:
Perl中面向对象的异常处理
异常与状态返回
那些可以让你进一步阅读的好链接.
我永远不会混淆这两个模型......当你从使用错误代码的堆栈的一部分移动到使用异常的更高部分时,从一个模型转换到另一个模型太难了.
例外情况是"任何阻止或禁止方法或子程序执行您要求它做的事情"......不要传递有关不规则或异常情况的消息,或系统状态等.使用返回值或参考(或出)参数.
异常允许使用依赖于方法函数的语义来编写(和利用)方法,即可以键入返回Employee对象或Employees列表的方法来执行此操作,并且可以通过调用来使用它.
Employee EmpOfMonth = GetEmployeeOfTheMonth();
使用错误代码,所有方法都返回错误代码,因此,对于需要返回调用代码使用的其他内容的人,您必须传递一个引用变量以填充该数据,并测试返回值在每个函数或方法调用上错误代码并处理它.
Employee EmpOfMonth; if (getEmployeeOfTheMonth(ref EmpOfMonth) == ERROR) // code to Handle the error here
如果你编写代码使每个方法只做一个简单的事情,那么只要方法无法实现方法的预期目标,就应该抛出异常.与错误代码相比,异常更加丰富且易于使用.您的代码更清晰 - "正常"代码路径的标准流程可以严格用于方法IS能够完成您希望它执行的操作的情况......然后代码清理或处理当发生阻止方法成功完成的不良事件时,"异常"情况可能会偏离正常代码.此外,如果您无法处理发生的异常,并且必须将其向上传递到UI,(或者更糟糕的是,从中间层组件到UI的线路),然后使用异常模型,
在过去,我加入了错误代码阵营(做了太多的C编程).但现在我已经看到了光明.
是例外对系统来说是一个负担.但它们简化了代码,减少了错误数量(和WTF).
所以使用异常但明智地使用它们.他们将成为你的朋友.
作为旁注.我已经学会了记录哪个方法可以抛出哪个异常.不幸的是,大多数语言都不需要这样做.但它增加了在适当级别处理正确异常的机会.
在某些情况下,以干净,清晰,正确的方式使用异常是很麻烦的,但绝大多数时间异常是显而易见的选择.异常处理超过错误代码的最大好处是它改变了执行流程,这有两个重要原因.
发生异常时,应用程序不再遵循其"正常"执行路径.这一点非常重要的第一个原因是,除非代码的作者顺利完成并且真正让他们变得糟糕,否则该程序将停止并且不会继续执行不可预测的事情.如果未检查错误代码并且未采取适当的操作来响应错误的错误代码,程序将继续执行它正在执行的操作以及谁知道该操作的结果将是什么.有很多情况下让程序做"什么"可能会非常昂贵.考虑一个程序,该程序检索公司销售的各种金融工具的绩效信息,并将该信息传递给经纪人/批发商.如果出现问题并且程序继续运行,它可能会将错误的性能数据发送给经纪人和批发商.我不知道其他任何人,但我不想成为副总裁办公室的人,解释为什么我的代码导致该公司获得7位数的监管罚款.向客户传递错误消息通常比提供可能看起来"真实"的错误数据更可取,后一种情况更容易遇到错误代码等不那么激进的方法.
我喜欢异常和破坏正常执行的第二个原因是,它使得"正常事物发生"逻辑与"错误的逻辑"分开更容易,更容易.对我来说,这个:
try { // Normal things are happening logic catch (// A problem) { // Something went wrong logic }
......比这更好:
// Some normal stuff logic if (errorCode means error) { // Some stuff went wrong logic } // Some normal stuff logic if (errorCode means error) { // Some stuff went wrong logic } // Some normal stuff logic if (errorCode means error) { // Some stuff went wrong logic }
关于异常的其他一些小问题也很好.有一堆条件逻辑来跟踪函数中调用的任何方法是否返回了错误代码,并返回错误代码更高的是很多锅炉板.事实上,很多锅炉板都可能出错.我对大多数语言的异常系统有更多的信心,而不是像弗雷德写的"大学新生"那样的if-else-if-else语句的老鼠窝,我有很多更好的事情要做我的时间比代码审查所说的老鼠的巢.
你应该同时使用两者.问题是决定何时使用每一个.
有几种情况下,例外是明显的选择:
在某些情况下,您无法对错误代码执行任何操作,并且您只需要在调用堆栈的上层处理它,通常只需记录错误,向用户显示内容或关闭程序.在这些情况下,错误代码将要求您逐级手动冒泡错误代码,这显然更容易处理异常.关键是这是出于意外和不可操作的情况.
然而关于情况1(发生意外和不可操作的事情,你只是想记录它),异常可能会有所帮助,因为你可能会添加上下文信息.例如,如果我在较低级别的数据助手中获得SqlException,我将希望在低级别(我知道导致错误的SQL命令)中捕获该错误,以便我可以捕获该信息并重新抛出其他信息.请注意这里的神奇词:重新抛出,而不是吞咽. 异常处理的第一条规则:不要吞下异常.另外,请注意我的内部catch不需要记录任何内容,因为外部catch将具有整个堆栈跟踪并可以记录它.
在某些情况下,您有一系列命令,如果其中任何一个命令失败,您应该清理/处置资源(*),无论这是不可恢复的情况(应该抛出)还是可恢复的情况(在这种情况下你可以本地或在调用者代码中处理,但您不需要例外).显然,将所有这些命令放在一次尝试中要容易得多,而不是在每个方法之后测试错误代码,并在finally块中进行清理/处理.请注意,如果你想让错误冒出来(这可能是你想要的),你甚至不需要抓住它 - 你只需要使用finally来进行清理/处理 - 如果你想要你应该只使用catch/retrow添加上下文信息(参见子弹2).
一个例子是事务块内的一系列SQL语句.同样,这也是一个"不可操作"的情况,即使你决定及早赶上它(在当地治疗而不是冒泡到顶部)它仍然是一个致命的情况,最好的结果是中止一切或至少中止一个大的这个过程的一部分.
(*)这就像on error goto
我们在旧的Visual Basic中使用的那样
在构造函数中,您只能抛出异常.
话虽如此,在您返回调用者CAN /应该采取某些行动的一些信息的所有其他情况下,使用返回码可能是更好的选择.这包括所有预期的"错误",因为可能它们应由直接调用者处理,并且几乎不需要在堆栈中冒出太多级别.
当然,总是可以将预期的错误视为异常,然后立即捕获上面的一个级别,并且还可以包含try catch中的每一行代码并对每个可能的错误采取操作.IMO,这是一个糟糕的设计,不仅因为它更冗长,而且特别是因为如果不阅读源代码,可能引发的可能异常并不明显 - 并且可以从任何深层方法抛出异常,从而创建不可见的getos.它们通过创建多个不可见的退出点来破坏代码结构,这使得代码难以阅读和检查.换句话说,您永远不应该将异常用作流控制,因为其他人很难理解和维护.甚至很难理解所有可能的代码流以进行测试.
再次:为了正确清理/处置,你可以使用try-finally而不会捕获任何东西.
关于返回码的最流行的批评是"有人可以忽略错误代码,但同样的意义上,有人也可以吞下异常.两种方法中的异常处理都很容易.但编写好的基于错误代码的程序仍然要容易得多比写一个基于异常情况的程序.如果一个以任何理由决定忽略所有的错误(旧on error resume next
),你可以很容易地做到这一点与返回代码,你不能这样做,没有很多的尝试,抓样板.
关于返回码的第二个最受欢迎的批评是"它很难冒泡" - 但这是因为人们不理解异常是针对不可恢复的情况,而错误代码则不是.
异常和错误代码之间的决定是一个灰色区域.您甚至可能需要从某些可重用的业务方法获取错误代码,然后您决定将其包装到异常(可能添加信息)中并让它冒泡.但是假设所有错误都应该作为异常抛出,这是一个设计错误.
把它们加起来:
我喜欢在遇到意外情况时使用异常,其中没有太多事情可做,通常我们想要中止大量代码甚至整个操作或程序.这就像旧的"on error goto".
我希望在我预期调用者代码可以/应该采取某些操作的情况下使用返回代码.这包括大多数业务方法,API,验证等.
异常和错误代码之间的这种差异是GO语言的设计原则之一,它对致命的意外情况使用"恐慌",而常规预期情况作为错误返回.
然而关于GO,它还允许多个返回值,这对使用返回代码有很大帮助,因为您可以同时返回错误和其他内容.在C#/ Java上,我们可以通过out参数,Tuples或(我最喜欢的)Generics实现这一点,它与枚举相结合可以为调用者提供清晰的错误代码:
public MethodResult CreateOrder(CreateOrderOptions options)
{
....
return MethodResult.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area");
...
return MethodResult.CreateSuccess(CreateOrderResultCodeEnum.SUCCESS, order);
}
var result = CreateOrder(options);
if (result.ResultCode == CreateOrderResultCodeEnum.OUT_OF_STOCK)
// do something
else if (result.ResultCode == CreateOrderResultCodeEnum.SUCCESS)
order = result.Entity; // etc...
如果我在我的方法中添加一个新的可能返回,我甚至可以检查所有调用者是否在switch语句中覆盖了这个新值.除了例外,你真的不能这样做.当您使用返回代码时,您通常会事先知道所有可能的错误,并对它们进行测试.除了例外,您通常不知道会发生什么.将枚举包含在异常(而不是泛型)中是另一种选择(只要每个方法都会抛出异常的类型),但IMO仍然是糟糕的设计.