昨天我和同事讨论了什么是首选的错误报告方法。主要是我们讨论了在应用程序层或模块之间报告错误的异常或错误代码的使用。
您使用哪些规则来决定是否抛出异常或返回错误代码以进行错误报告?
答案 0 :(得分:75)
在高级别的东西中,例外;在低级别的东西,错误代码。
异常的默认行为是展开堆栈并停止程序,如果我正在写一个脚本并且我找到一个不在字典中的密钥它可能是一个错误,我希望程序停止让我知道这一切。
但是,如果我正在编写一段代码,我必须知道在每种可能情况下的行为,那么我想要错误代码。否则,我必须知道我的函数中的每一行都可以抛出的每个异常,以了解它将做什么(阅读The Exception That Grounded an Airline以了解这是多么棘手)。编写对每种情况都适当做出反应的代码(包括不愉快的代码)是很繁琐和困难的,但那是因为编写无错误的代码既乏味又难,不是因为你传递了错误代码。
两个Raymond Chen and Joel都提出了一些反对对所有事情使用例外的雄辩论点。
答案 1 :(得分:54)
我通常更喜欢异常,因为它们有更多的上下文信息,并且能够以更清晰的方式向程序员传达(如果使用得当)错误。
另一方面,错误代码比异常更轻量级但更难维护。错误检查可能会无意中被省略。错误代码更难维护,因为您必须保留包含所有错误代码的目录,然后打开结果以查看引发的错误。错误范围在这里可能有所帮助,因为如果我们唯一感兴趣的是如果我们是否存在错误,则检查更简单(例如,大于或等于0的HRESULT错误代码是成功的,小于零就是失败)。它们可能会被无意中省略,因为没有编程强制开发人员会检查错误代码。另一方面,你不能忽视例外。
总结一下,我几乎在所有情况下都偏好错误代码。
答案 2 :(得分:23)
我更喜欢例外,因为
答案 3 :(得分:18)
您的函数的调用者可以忽略错误代码(通常是!)。例外至少迫使他们以某种方式处理错误。即使他们处理它的版本是一个空的捕获处理程序(叹气)。
答案 4 :(得分:16)
错误代码的例外,毫无疑问。与错误代码一样,您可以获得与异常相同的好处,而且还有更多错误代码的缺点。唯一的例外是它的开销略高;但是在这个时代,对于几乎所有的应用来说,这个开销应该被认为是微不足道的。
以下是一些讨论,比较和对比这两种技术的文章:
那些可以让你进一步阅读的好链接。
答案 5 :(得分:14)
我永远不会混淆这两个模型......当你从使用错误代码的堆栈的一部分移动到使用异常的更高部分时,从一个模型转换到另一个模型太难了。
例外情况是“任何阻止或禁止方法或子程序执行你要求它做的事情”......不要传递有关不规则或异常情况或系统状态等的消息。使用返回值的参数或参考(或出)参数。
异常允许使用依赖于方法函数的语义来编写(和利用)方法,即可以键入返回Employee对象或Employees列表的方法来执行此操作,并且可以通过调用来使用它。
Employee EmpOfMonth = GetEmployeeOfTheMonth();
使用错误代码,所有方法都返回错误代码,因此,对于那些需要返回调用代码要使用的其他内容的人,您必须传递一个引用变量以填充该数据,并测试返回在每个函数或方法调用上,错误代码的值,并处理它。
Employee EmpOfMonth;
if (getEmployeeOfTheMonth(ref EmpOfMonth) == ERROR)
// code to Handle the error here
如果你编写代码使得每个方法只做一个简单的事情,那么只要方法无法实现方法的预期目标,就应该抛出异常。与错误代码相比,异常更加丰富且易于使用。您的代码更清晰 - “正常”代码路径的标准流程可以严格用于方法IS能够完成您希望它执行的操作的情况......然后代码清理或处理当发生阻止方法成功完成的不良事件时,“异常”情况可能会偏离正常代码。另外,如果您无法处理发生的异常,并且必须将其传递到UI,(或者更糟糕的是,从中间层组件到UI的线路),然后使用异常模型,不需要对堆栈中的每个介入方法进行编码来识别并将异常传递到堆栈中...异常模型会自动为您执行此操作....使用错误代码,此拼图可以非常快速地繁重
答案 6 :(得分:11)
过去我加入了错误代码阵营(做了太多的C编程)。但现在我已经看到了光明。
是的例外对系统来说是一个负担。但它们简化了代码,减少了错误数量(和WTF)。
所以使用异常但明智地使用它们。他们将成为你的朋友。
作为旁注。我已经学会了记录哪个方法可以抛出哪个异常。不幸的是,大多数语言都不需要这样做。但它增加了在适当级别处理正确例外的机会。
答案 7 :(得分:10)
在某些情况下,以干净,清晰,正确的方式使用异常是很麻烦的,但绝大多数时间异常是显而易见的选择。最大的好处是异常处理超过了错误代码,它改变了执行流程,这有两个重要原因。
当发生异常时,应用程序不再遵循它的常规'执行路径。这一点非常重要的第一个原因是,除非代码的作者顺利完成并且真正地让他们变坏,否则程序将停止并且不会继续做不可预测的事情。如果没有检查错误代码并且没有采取适当的操作来响应错误的错误代码,程序将继续执行它正在做的事情以及谁知道该操作的结果是什么将会。在很多情况下,程序都可以做任何事情。可能会非常昂贵。考虑一个程序,该程序检索公司销售的各种金融工具的绩效信息,并将该信息传递给经纪人/批发商。如果出现问题并且程序继续运行,它可能会将错误的性能数据发送给经纪人和批发商。我不了解其他任何人,但我不想成为副总裁办公室的一员,解释为什么我的代码导致该公司获得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语句的大鼠巢,这些语句是“大学毕业后的”#39; Fresh-out-of-college'弗雷德写道,我和我的时间有很多关系,而不是代码审查老鼠的巢。
答案 8 :(得分:5)
你应该同时使用两者。问题是决定何时使用每一个。
几种情况下,例外是明显的选择:
在某些情况下您无法使用错误代码执行任何操作,并且您只需需要在调用堆栈的上层处理它,通常只记录错误,向用户显示内容或关闭程序。在这些情况下,错误代码将要求您逐级手动冒泡错误代码,这显然更容易处理异常。重点是,这是针对意外和不可处理的情况。
然而关于情况1(发生意外和不可操作的事情,你只是想记录它),异常可能会有所帮助,因为你可能添加上下文信息。例如,如果我在我的低级数据助手中得到SqlException,我会想要在低级别(我知道导致错误的SQL命令)中捕获该错误,因此我可以捕获该信息并重新抛出以及其他信息。请注意这里的神奇词:重新抛出,而不是吞下。 The first rule of exception handling: do not swallow exceptions。另外,请注意我的内部catch并不需要记录任何内容,因为外部catch将具有整个堆栈跟踪并可以记录它。
在某些情况下,您有一系列命令,如果其中任何一个失败您应该清理/处置资源(*),无论这是否是一种不可恢复的情况(应该抛出)或可恢复的情况(在这种情况下,您可以在本地或在调用者代码中处理,但您不需要例外)。显然,将所有这些命令放在一次尝试中要容易得多,而不是在每个方法之后测试错误代码,并在finally块中进行清理/处理。请注意如果你想让错误冒出来(这可能是你想要的),你甚至不需要抓住它 - 你只需要使用finally来清理/处置 - 你如果要添加上下文信息,应该只使用catch / retrow(参见子弹2)。
一个例子是事务块内的一系列SQL语句。同样,这也是一个不可理解的&#34;情况,即使你决定及早赶上它(在当地治疗而不是冒泡到顶部)它仍然是一个<强>致命的情况从最好的结果是中止一切或至少中止了大部分过程。
(*)这就像我们在旧的Visual Basic中使用的on error goto
在构造函数中,您只能抛出异常。
话虽如此,在您回复来电者CAN /应该采取某些行动的所有其他情况下,使用返回代码可能是更好的选择。这包括所有预期&#34;错误&#34; ,因为可能它们应由直接调用者处理,并且几乎不需要在堆栈中冒出太多级别。
当然,总是可以将预期的错误视为异常,然后立即捕获上一级,并且还可以包含try catch中的每一行代码并为每个代码执行操作可能的错误。 IMO,这是一个糟糕的设计,不仅因为它更冗长,而且特别是因为如果不阅读源代码,可能引发的可能异常并不明显 - 并且可以从任何深层方法抛出异常,创建{ {3}}。它们通过创建多个不可见的退出点来破坏代码结构,这使得代码难以阅读和检查。换句话说,您绝不应将例外用作invisible gotos ,因为其他人很难理解和维护。甚至很难理解所有可能的代码流以进行测试 再次:正确清理/处置你可以使用try-finally而不会捕获任何东西。
关于返回码的最受欢迎的批评是&#34;有人可以忽略错误代码,但在同样的意义上,某人也可以吞下异常。 flow-control。如果出于任何原因决定忽略所有错误(旧on error resume next
),您可以使用返回代码轻松完成此操作,如果没有大量的try-catchs样板,您就无法做到这一点。
关于返回代码的第二个最受欢迎的批评是“它很难冒泡”#34; - 但那是因为人们不了解异常是针对不可恢复的情况,而错误代码则不是。
异常和错误代码之间的确定是一个灰色区域。您甚至可能需要从某些可重用的业务方法中获取错误代码,然后您决定将其包装到异常(可能添加信息)中并让它冒泡。但是,假设所有错误都应该作为异常抛出,这是一个设计错误。
总结一下:
我喜欢在遇到意外情况时使用例外情况,因为我们不想做很多事情,通常我们想要中止大量代码甚至整个操作或程序。这就像旧的&#34;错误goto&#34;。
我希望在我预期调用者代码可以/应该采取某些操作的情况下使用返回代码。这包括大多数业务方法,API,验证等。
异常和错误代码之间的这种差异是GO语言的设计原则之一,它使用&#34; panic&#34;对于致命的意外情况,而常规预期情况将作为错误返回。
然而关于GO,它还允许Bad exception handling is easy in both methods. But writing good error-code-based program is still much easier than writing an exception-based program,这对使用返回代码有很大帮助,因为您可以同时返回错误和其他内容。在C#/ Java上,我们可以使用out参数,Tuples或(我最喜欢的)Generics实现这一点,它与枚举相结合可以为调用者提供清晰的错误代码:
public MethodResult<CreateOrderResultCodeEnum, Order> CreateOrder(CreateOrderOptions options)
{
....
return MethodResult<CreateOrderResultCodeEnum>.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area");
...
return MethodResult<CreateOrderResultCodeEnum>.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仍然是糟糕的设计。
答案 9 :(得分:4)
我可能会坐在这里,但是......
在Python中,使用异常是标准做法,我很乐意定义自己的异常。在C中你根本没有例外。
在C ++中(至少在STL中),异常通常仅针对真正异常的错误抛出(我几乎从未见过它们)。我认为没有理由在我自己的代码中做任何不同的事情。是的,很容易忽略返回值,但C ++也不会强迫您捕获异常。我认为你必须养成这样做的习惯。
我所使用的代码库主要是C ++,我们几乎在任何地方使用错误代码,但是有一个模块可以引发任何错误的异常,包括非常普遍的错误,并且所有使用该模块的代码都非常糟糕。但这可能只是因为我们混合了异常和错误代码。始终使用错误代码的代码更容易使用。如果我们的代码一直使用异常,也许它不会那么糟糕。将两者混合似乎不太好用。
答案 10 :(得分:4)
由于我使用C ++,并使用RAII使其安全使用,因此我几乎只使用异常。它将错误处理从正常的程序流中拉出来并使意图更加清晰。
我确实为特殊情况留下了例外情况。如果我预计某个错误会发生很多,我会在执行之前检查操作是否成功,或者调用使用错误代码的函数版本(如TryParse()
)
答案 11 :(得分:3)
我的理由是,如果您正在编写一个真正需要性能的低级驱动程序,那么请使用错误代码。但是如果你在更高级别的应用程序中使用该代码并且它可以处理一些开销,那么用一个检查这些错误代码并引发异常的接口包装该代码。
在所有其他情况下,例外可能是要走的路。
答案 12 :(得分:3)
我的方法是我们可以同时使用两者,即异常和错误代码。
我习惯于定义几种类型的异常(例如:DataValidationException或ProcessInterruptExcepion),并在每个异常中定义每个问题的更详细描述。
Java中的一个简单示例:
public class DataValidationException extends Exception {
private DataValidation error;
/**
*
*/
DataValidationException(DataValidation dataValidation) {
super();
this.error = dataValidation;
}
}
enum DataValidation{
TOO_SMALL(1,"The input is too small"),
TOO_LARGE(2,"The input is too large");
private DataValidation(int code, String input) {
this.input = input;
this.code = code;
}
private String input;
private int code;
}
通过这种方式,我使用Exceptions定义类别错误,并使用错误代码来定义有关问题的更详细信息。
答案 13 :(得分:3)
方法签名应该告诉您方法的作用。就像是 long errorCode = getErrorCode(); 可能没问题,但是 long errorCode = fetchRecord(); 令人困惑。
答案 14 :(得分:2)
异常是针对异常的情况 - 即,当它们不属于正常的代码流时。
混合异常和错误代码是非常合理的,其中错误代码表示某些内容的状态,而不是代码本身运行中的错误(例如,检查来自子进程的返回代码)。
但是当出现异常情况时,我认为例外是最具表现力的模式。
在某些情况下,您可能更喜欢或不得使用错误代码来代替异常,这些已经被充分涵盖(除了其他明显的约束,例如编译器支持)。
但是,在另一个方向上,使用Exceptions可以为错误处理构建更高级别的抽象,这可以使您的代码更具表现力和自然性。我强烈推荐阅读这篇由C ++专家Andrei Alexandrescu撰写的关于他所谓的“执行”主题的优秀但被低估的文章:http://www.ddj.com/cpp/184403864。虽然这是一篇C ++文章,但这些原则通常是适用的,我已经将强制执行概念非常成功地翻译成了C#。
答案 15 :(得分:2)
首先,我同意Tom的answer对于高级别的东西使用异常,并且对于低级别的东西使用错误代码,只要它不是面向服务的体系结构(SOA)。
在SOA中,可以跨不同的机器调用方法,异常可能不会通过线路传递,相反,我们使用具有如下结构的成功/失败响应(C#):
public class ServiceResponse
{
public bool IsSuccess => string.IsNullOrEmpty(this.ErrorMessage);
public string ErrorMessage { get; set; }
}
public class ServiceResponse<TResult> : ServiceResponse
{
public TResult Result { get; set; }
}
并像这样使用:
public async Task<ServiceResponse<string>> GetUserName(Guid userId)
{
var response = await this.GetUser(userId);
if (!response.IsSuccess) return new ServiceResponse<string>
{
ErrorMessage = $"Failed to get user."
};
return new ServiceResponse<string>
{
Result = user.Name
};
}
当在服务响应中一致地使用它们时,它会在应用程序中创建处理成功/失败的非常好的模式。这样可以在服务内和服务中的异步调用中更轻松地处理错误。
答案 16 :(得分:1)
我更喜欢所有错误情况的异常,除非失败是返回原始数据类型的函数的可预期的无错误结果。例如。如果找不到,则在较大字符串中查找子字符串的索引通常会返回-1,而不是引发NotFoundException。
返回可能被解除引用的无效指针(例如,在Java中导致NullPointerException)是不可接受的。
使用多个不同的数字错误代码(-1,-2)作为同一函数的返回值通常是错误的样式,因为客户端可能会执行“== -1”检查而不是“&lt; 0”。
这里要记住的一件事是API随着时间的推移而发展。良好的API允许以多种方式更改和扩展故障行为,而不会破坏客户端。例如。如果客户端错误句柄检查了4个错误情况,并且您向函数添加了第五个错误值,则客户端处理程序可能无法测试此错误并中断。如果您引发了例外,这通常会使客户更容易迁移到更新版本的库。
另一件需要考虑的事情是,在团队中工作时,为所有开发人员制定明确界限以做出这样的决定。例如。 “高级别东西的例外,低级别东西的错误代码”是非常主观的。
在任何情况下,如果可能存在多个简单类型的错误,源代码绝不应该使用数字文字来返回错误代码或处理错误代码(如果x == -7则返回-7 ... ),但总是一个命名常量(返回NO_SUCH_FOO,如果x == NO_SUCH_FOO)。
答案 17 :(得分:1)
如果您在大项目下工作,则不能仅使用例外或仅使用错误代码。在不同情况下,您应该使用不同的方法。
例如,您决定仅使用例外。但是一旦你决定使用异步事件处理。在这种情况下使用异常进行错误处理是个坏主意。但是在应用程序中到处使用错误代码是单调乏味的。
所以我认为同时使用异常和错误代码是正常的。
答案 18 :(得分:0)
对于大多数应用程序,例外情况更好。例外情况是软件必须与其他设备通信。我工作的领域是工业控制。这里错误代码是首选和预期的。所以我的答案是它确实取决于具体情况。
答案 19 :(得分:0)
我认为这还取决于你是否真的需要结果中的堆栈跟踪等信息。如果是的话,你肯定会选择Exception,它提供了大量关于问题的信息。但是,如果您只对结果感兴趣并且不关心为什么该结果会出现错误代码。
e.g。当您处理文件并面对IOException时,客户端可能有兴趣知道触发的位置,打开文件或解析文件等。因此,最好返回IOException或其特定的子类。但是,像你有登录方法的场景,你想知道它是否成功,你要么只返回布尔值或显示正确的消息,返回错误代码。这里客户端不知道哪个部分的逻辑导致了错误代码。他只知道其凭证无效或帐户锁定等。
我能想到的另一个用例是数据在网络上传播。您的远程方法只能返回错误代码而不是Exception,以最大限度地减少数据传输。
答案 20 :(得分:0)
我的一般规则是:
答案 21 :(得分:-1)
当您的方法返回除数值以外的任何内容时,错误代码也不起作用...