例外或错误代码

昨天我和一个同事进行了一场激烈的辩论,首先是错误报告方法。 主要是讨论使用exception或错误代码来报告应用程序层或模块之间的错误。

您使用什么规则来决定是否抛出exception或返回错误报告的错误代码?

在高层次的东西,例外; 在低级的东西,错误代码。

一个exception的默认行为是展开堆栈并停止程序,如果我正在编写一个脚本,我去找一个不在字典中的键,这可能是一个错误,我希望程序停止,让我知道这一切。

但是,如果我写了一段代码,我必须知道每种可能情况下的行为,那么我需要错误代码。 否则,我必须知道我的函数中每一行可能抛出的每一个exception,以了解它会做什么(阅读航空公司的例外,以了解这是多么棘手)。 编写能够适应各种情况(包括不愉快的代码)的代码是很乏味和困难的,但这是因为编写无错代码是单调乏味的,而不是因为你传递了错误代码。

Raymond Chen 和 Joel都提出了一些有力的论据来反对所有的例外。

我通常更喜欢exception,因为他们有更多的上下文信息,并能够以更清晰的方式向程序员传达(如果正确使用)错误。

另一方面,错误代码比例外更轻,但难以维护。 错误检查可能会被无意中忽略。 错误代码难以维护,因为您必须保留包含所有错误代码的目录,然后打开结果以查看抛出的错误。 错误范围可以在这里帮助,因为如果我们唯一感兴趣的是如果我们在出现错误或不存在,检查(例如,大于或等于0的HRESULT错误代码是成功和小于零就是失败)。 他们可能会被忽略,因为没有程序强制开发人员会检查错误代码。 另一方面,你不能忽视exception。

总结一下,在几乎所有情况下,我都喜欢在错误代码上使用exception。

我喜欢例外,因为

  • 他们中断了逻辑stream程
  • 他们受益于提供更多function的类层次结构
  • 当正确使用时可以表示各种各样的错误(例如,InvalidMethodCallException也是一个LogicException,因为两者都是在代码中存在应该在运行时之前检测到的错误时发生的),以及
  • 他们可以用来增强错误(即FileReadException类的定义可以包含代码来检查文件是否存在或被locking等)

错误代码可以被你的函数的调用者忽略(通常是!)。 例外情况至less迫使他们以某种方式处理错误。 即使他们处理它的版本是有一个空的catch处理程序(叹气)。

错误代码的例外,毫无疑问。 与错误代码一样,您可以从exception中获得许多相同的好处,但是也可以避免错误代码的缺点。 唯一的例外情况是它的开销略高。 但是在这个时代,几乎所有的应用程序都应该忽视这个开销。

以下是一些讨论,比较和对比两种技术的文章:

  • 面向对象的Perlexception处理
  • exception与状态返回

在那些可以给你进一步阅读的链接中有一些很好的链接。

我绝对不会混用这两个模型……当你从使用错误代码的堆栈的一部分转移到使用exception的较高部分时,从一个转换到另一个是非常困难的。

例外情况是“任何阻止或禁止方法或子程序做你所要求做的事情……”而不是传递有关违规或exception情况或系统状态等的消息。使用返回值或参考(或输出)参数。

例外允许方法被写入(和利用)依赖于方法function的语义,也就是说返回一个Employee对象或者一个Employees列表的方法可以被input来做到这一点,你可以通过调用来使用它。

 Employee EmpOfMonth = GetEmployeeOfTheMonth(); 

有了错误代码,所有的方法都会返回一个错误代码,因此,对于那些需要返回其他东西以供调用代码使用的代码,您必须传递一个引用variables来填充该数据,然后testing返回值错误代码,并在每个函数或方法调用中处理它。

 Employee EmpOfMonth; if (getEmployeeOfTheMonth(ref EmpOfMonth) == ERROR) // code to Handle the error here 

如果你编写代码,以便每个方法只有一个简单的事情,那么你应该抛出一个exception,只要方法不能实现方法的目标。 例外情况比错误代码更加丰富和易于使用。 你的代码更干净 – “正常”代码path的标准stream程可以严格地用于方法IS能够完成你想要它做的事情…然后代码清理,或处理当不正常的事情发生,阻止方法成功完成的“例外”情况可以从正常的代码中分离出来。 另外,如果你不能处理发生的exception,并且必须把堆栈传递给一个UI(或者更糟的是,从一个中间层组件到一个UI),然后通过exception模型不需要编码栈中的每一个干预方法来识别并传递exception堆栈…exception模型为你自动完成这个工作…使用错误代码,这个难题可能非常快。

可能有几种情况,以清晰,清晰,正确的方式使用例外是麻烦的,但绝大多数时间例外是明显的select。 exception处理对错误代码的最大好处是它改变了执行的stream程,这有两个重要的原因。

发生exception时,应用程序不再遵循“正常”执行path。 第一个原因如此重要的原因是,除非编码的作者顺利完成,否则这个编程就会停止,不会继续做不可预测的事情。 如果一个错误代码没有被检查,并且没有采取适当的措施来响应错误的错误代码,那么程序会继续做它正在做的事情,谁知道这个行为的结果会是什么。 有很多情况下,程序做“任何”可能会非常昂贵。 考虑一个程序,检索公司销售的各种金融工具的performance信息,并将这些信息传递给经纪人/批发商。 如果出现问题,程序继续进行,可能会向经纪商和批发商发送错误的业绩数据。 我不知道其他人,但我不想成为副总裁办公室的一员,解释为什么我的代码导致公司获得7位数的监pipe罚款。 向客户发送错误信息通常比提供可能看起来是“真实的”的错误数据更可取,而后一种情况则更容易使用诸如错误代码等较不积极的方法。

我喜欢exception和打破正常执行的第二个原因是,使得“正常的事情正在发生”的逻辑与“出错的逻辑”分开的事情变得容易得多。 对我来说,这个:

 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 } 

还有其他一些关于exception的小东西也很好。 有一堆条件逻辑来跟踪函数中调用的任何方法是否返回了一个错误代码,并且返回错误代码更多的是锅炉板。 事实上,很多锅炉板可能会出问题。 我对大多数语言的例外系统有更多的信心,比我写的Fred's写的“大学新生”这样的if-else-if-else语句的老鼠窝更有信心,而且我还有很多更好的事情要做我的时间比代码复习鼠窝。

在过去,我join了错误代码阵营(做了太多的C编程)。 但现在我已经看到了光。

是的例外是有点负担的系统。 但他们简化了代码,减less了错误(和WTF)的数量。

所以使用exception,但明智地使用它们。 他们将成为你的朋友。

作为一个方面说明。 我已经学会logging哪种方法可以抛出哪个exception。 不幸的是,这是大多数语言不要求的。 但它增加了处理正确的例外的机会。

方法签名应该告诉你方法做了什么。 类似long errorCode = getErrorCode(); 可能会很好,但long errorCode = fetchRecord(); 很混乱

我的推理是如果你正在编写一个真正需要性能的低级驱动程序,然后使用错误代码。 但是,如果您在较高级别的应用程序中使用该代码,并且可以处理一些开销,则可以使用检查这些错误代码并引发exception的接口来包装该代码。

在所有其他情况下,例外情况可能是最好的select。

我的方法是我们可以同时使用两个,即exception和错误代码。

我习惯于定义几种types的exception(例如:DataValidationException或ProcessInterruptExcepion),在每个exception中定义每个问题的更详细的描述。

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; } 

通过这种方式,我使用exception来定义类别错误,并使用错误代码来定义有关该问题的更详细的信息。

我可能坐在这里的栅栏上,但是…

  1. 这取决于语言。
  2. 你select哪种模式,对你如何使用它是一致的。

在Python中,使用exception是标准做法,我很高兴定义自己的exception。 在C中你根本没有例外。

在C ++中(至less在STL中),exception通常只会抛出真正的exception错误(我几乎从来没有看到它们自己)。 我没有理由在自己的代码中做任何不同的事情。 是的,忽略返回值很容易,但是C ++不会强制你捕获exception。 我想你只是养成这样做的习惯。

我所使用的代码基本上是C ++,我们几乎在任何地方都使用错误代码,但是有一个模块为任何错误引发exception,包括非常普通的错误,所有使用该模块的代码都非常糟糕。 但这可能是因为我们混合了exception和错误代码。 始终使用错误代码的代码更容易处理。 如果我们的代码一直使用exception,也许它不会那么糟糕。 混合这两个似乎并不太好。

由于我使用C ++,并且让RAII使它们安全使用,所以几乎全部使用exception。 它将error handling从正常的程序stream程中解脱出来,使意图更加清晰。

尽pipe如此,我确实在例外情况下留下了例外 如果我期待某个错误将会发生很多,我会在执行之前检查操作是否成功,或者调用一个使用错误代码的函数版本(如TryParse()

例外情况是特殊的情况 – 即,当它们不是正常代码stream程的一部分时。

混合exception和错误代码是非常合理的,其中错误代码代表某事物的状态,而不是代码本身运行中的错误(例如,检查来自subprocess的返回代码)。

但是,当一个特殊的情况发生时,我认为例外是最具performance力的模式。

在某些情况下,您可能更喜欢使用错误代码来代替例外情况,并且已经充分涵盖了这些情况(除了编译器支持等其他明显的限制外)。

但是从另一个angular度来看,使用Exceptions可以让你为你的error handlingbuild立更高层次的抽象,这可以使你的代码更具performance力和自然性。 我强烈build议阅读C ++专家Andrei Alexandrescu关于他所谓的“Enforcements”主题的优秀而又低估的文章: http : //www.ddj.com/cpp/184403864 。 尽pipe这是一篇C ++文章,但是这些原则通常是可以应用的,而且我已经很成功地将执行概念转换为C#了。

首先,我同意汤姆的回答 :对于高层次的东西使用exception,对于低层次的东西使用错误代码,只要它不是面向服务的架构(SOA)。

在SOA中,可以通过不同的机器调用方法,exception可能不会通过线路传递,而是使用具有如下结构(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 }; } 

当这些在服务响应中一致使用时,它会创build一个很好的模式来处理应用程序中的成功/失败。 这允许在服务以及跨服务的asynchronous调用中更容易的error handling。

对于所有的错误情况,我宁愿使用exception,除非失败是返回原始数据types的函数的可预期的无错误结果。 例如,如果找不到更大string中的子string的索引,则通常返回-1,而不是引发NotFoundException。

返回可能被解除引用的无效指针(例如,在Java中导致NullPointerException)是不可接受的。

使用多个不同的数字错误代码(-1,-2)作为相同函数的返回值通常是不好的风格,因为客户端可能会执行“== -1”检查而不是“<0”。

有一点需要记住的是随着时间的推移API的发展。 一个好的API允许以多种方式改变和扩展故障行为,而不会中断客户端。 例如,如果客户端error handling检查了4个错误情况,并且向函数添加了第五个错误值,则客户端处理程序可能不会testing这个错误并中断。 如果引发exception,这通常会使客户更容易迁移到较新版本的库。

另外要考虑的是在一个团队中工作,在那里为所有的开发人员做出这样的决定。 例如“高级别的例外,低级别的错误代码”是非常主观的。

在任何情况下,如果可能有多个不重要的types的错误,源代码不应该使用数字文字来返回错误代码或处理它(返回-7,如果x == -7 …),但总是一个命名常量(返回NO_SUCH_FOO,如果x == NO_SUCH_FOO)。

如果您在大项目下工作,则不能只使用exception或仅使用错误代码。 在不同情况下,您应该使用不同的方法。

例如,您决定只使用例外。 但是一旦你决定使用asynchronous事件处理。 在这种情况下,使用exception来处理错误是个坏主意。 但是在应用程序中使用错误代码是很乏味的。

所以我认为同时使用exception和错误代码是正常的。

对于大多数应用程序,例外情况会更好 软件必须与其他设备通信的例外情况。 我工作的领域是工业控制。 这里错误代码是优选的和预期的。 所以我的答案是,这取决于情况。

我想这也取决于你是否真的需要像结果中的堆栈跟踪信息。 如果是的话,你一定要去提供充满大量有关问题的信息的Exception。 但是,如果你只是对结果感兴趣,而不在乎为什么那个结果会出现错误代码。

例如,当你正在处理文件并面​​对IOException时,客户端可能会感兴趣的是从何处触发,在打开文件或parsing文件等等。所以最好返回IOException或其特定的子类。 但是,像你的情况下有login方法,你想知道它是成功与否,要么你只是返回布尔值或显示正确的消息,返回错误代码。 这里客户端不知道哪个部分的逻辑导致了错误代码。 他只是知道它的凭据无效或帐户锁等

我能想到的另一个用例是数据在networking上传输。 您的远程方法可以只返回错误代码而不是exception,以尽量减less数据传输。

我的一般规则是:

  • 在函数中只能出现一个错误:使用错误代码(作为函数的参数)
  • 可能会出现多个特定的错误:抛出exception

你应该使用两者。 事情是决定何时使用每一个

一些情况下,例外是明显的select

  1. 在某些情况下, 你不能对错误代码做任何事情 ,你只需要在调用堆栈中的上层处理它 ,通常只是logging错误,向用户显示或closures程序。 在这些情况下,错误代码会要求您逐级手动启动错误代码,这显然更容易处理exception。 关键是这是出乎意料和无法解决的情况。

  2. 然而关于情况1(意外和不可处理的情况发生,你只是不想logging),例外可能是有帮助的,因为你可能会添加上下文信息 。 例如,如果我在我的低级数据帮助程序中遇到SqlException,我将要在低级别(在那里我知道导致错误的SQL命令)中捕获该错误,以便我可以捕获该信息用附加信息重新抛出 。 请注意这里的魔语: 重新抛出,而不是吞下去 。 exception处理的第一条规则: 不要吞下exception 。 此外,请注意,我的内部捕获不需要logging任何东西,因为外部捕获将有整个堆栈跟踪,并可能会logging它。

  3. 在某些情况下,你有一系列的命令, 如果其中任何一个失败,你应该清理/处置资源 (*),不pipe这是一个不可恢复的情况(应该抛出)还是可恢复的情况(在这种情况下,你可以在本地或在调用者代码中处理,但不需要例外)。 显然,把所有这些命令放在一个单独的try中,而不是在每个方法后面testing错误代码,并在finally块中清理/处理,会容易得多。 请注意, 如果你想让错误冒出来(这可能是你想要的),你甚至不需要去捕捉它 – 你只需要使用finally进行清理/处理 – 如果你想要只使用catch / retrow添加上下文信息(参见第2项)。

    一个例子是一个事务块内的一系列SQL语句。 再次,这也是一个“难以解决”的情况,即使你决定早点把握(在本地对待而不是冒泡到顶端),它仍然是一个致命的情况,从最好的结果是放弃一切或至less放弃一个大的过程的一部分。
    (*)这就像我们在旧的Visual Basic中使用的on error goto

  4. 在构造函数中,你只能抛出exception。

话虽如此,在所有其他情况下,你要返回一些信息的主叫方可以采取一些行动 ,使用返回码可能是一个更好的select。 这包括了所有预期的“错误” ,因为它们应该由直接调用者来处理,并且几乎不需要在堆栈中冒出太多的层次。

当然总是可以将预期的错误当作exception来处理,然后立即捕捉到上面的一个层次,也可以在try catch中包含每一行代码,并为每个可能的错误采取行动。 国际海事组织,这是不好的devise,不仅因为它更详细,但特别是因为可能抛出的exception没有阅读源代码是不明显的 – 而且可以从任何深层的方法抛出exception,创build不可见的goto 。 他们通过创build多个看不见的退出点来破坏代码结构,使代码难以阅读和检查。 换句话说,你绝对不应该把exception用作stream量控制 ,因为那样对别人来说是难以理解和维护的。 甚至很难理解所有可能的testing代码stream。
再一次: 为了正确的清理/处理,你可以使用try-finally而不捕捉任何东西

关于返回码的最stream行的批评是,“有人可以忽略错误代码,但同样的,也有人可以吞下exception。 两种方法都不 处理exception ,但编写好的基于错误代码的程序仍然更容易而不是编写一个基于exception的程序 ,如果有人因为任何原因而决定忽略所有的错误(旧on error resume next ),你可以很容易地用返回代码来做到这一点,如果没有大量的try-catch样板。

关于返回码的第二个最受欢迎的批评是“很难冒泡” – 但是这是因为人们不理解exception是针对不可恢复的情况,而错误码则不是。

在exception和错误代码之间进行判断是一个灰色区域。 甚至有可能你需要从一些可重用的业务方法中获得一个错误代码,然后你决定把它包装成一个exception(可能是添加信息)并让它冒出来。 但是,假设所有错误都应该作为例外抛出是一个devise错误。

把它们加起来:

  • 我喜欢在出现意想不到的情况时使用exception,其中不需要做太多的事情,通常我们要中止一大块代码甚至整个操作或程序。 这就像旧的“错误转到”。

  • 当我预料到调用者代码可以/应该采取一些行动的情况时,我喜欢使用返回码。 这包括大多数业务方法,API和validation等。

exception和错误代码之间的区别是GO语言的devise原则之一,该语言使用“恐慌”来处理致命的意外情况,而常规的预期情况则作为错误返回。

然而关于GO,它还允许多个返回值 ,这对于使用返回码有很大的帮助,因为你可以同时返回一个错误和其他的东西。 在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语句中的新值。 你真的不能这样做,例外。 当您使用退货代码时,您通常会提前知道所有可能的错误,并对其进行testing。 有例外,你通常不知道会发生什么。 在exception(而不是generics)内部枚举枚举是一种替代方法(只要清楚每种方法将抛出的exception的types),但IMO仍然是不好的devise。

当您的方法返回除数值以外的任何内容时,错误代码也不起作用…