异常或错误代码约定


117

昨天,我与同事就首选的错误报告方法进行了激烈的辩论。主要是我们正在讨论使用异常或错误代码来报告应用程序层或模块之间的错误。

您使用什么规则来决定是否抛出异常或返回错误代码以进行错误报告?

Answers:


81

在高级内容中,例外;在低级的东西,错误代码。

异常的默认行为是解开堆栈并停止程序,如果我正在编写脚本,并且我寻找不在词典中的键,则可能是错误,并且我希望程序停止并让我知道全部。

但是,如果我正在编写一段必须知道在每种可能情况下的行为的代码,则我需要错误代码。否则,我必须知道函数中每行可能抛出的每个异常,才能知道其将要执行的操作(请阅读飞机停飞的异常》,以了解这有多棘手)。编写对每种情况(包括不愉快的情况)做出适当响应的代码既繁琐又困难,但这是因为编写无错误的代码既繁琐又困难,而不是因为您要传递错误代码。

无论雷蒙德陈 乔尔都反对使用异常的一切做了一些雄辩的论点。


5
+1指出上下文与错误处理策略有关。
alx9r 2012年

2
@Tom,很好,但可以保证会捕获异常。我们如何确保捕获错误代码并且不会由于错误而忽略它们?
Pacerier,2014年

13
因此,您唯一的反对异常的论据是,当您忘记捕获异常而又没有考虑忘记检查返回值是否存在错误的情况时,可能会发生某些不良情况?更不用说当您忘记捕获异常时您会得到堆栈跟踪,而当您忘记检查错误代码时却一无所获。
Esailija '16

3
C ++ 17引入了该nodiscard属性,如果未存储函数的返回值,该属性将向编译器发出警告。有助于捕获被遗忘的错误代码检查。例如:godbolt.org/g/6i6E0B
Zitrax

5
@Esailija的评论恰恰在这里。此处针对异常的论点采用了一个假设的基于错误代码的API,其中记录了所有错误代码;一个假设的程序员阅读了说明文件,正确识别了在其应用程序中逻辑上可能存在的所有错误情况,并编写了处理每个错误情况的代码,然后将该场景与假设的基于异常的API和程序员进行比较,在这种情况下,由于某种原因这些步骤之一出了问题...即使在基于异常的API中正确实现所有这些步骤同样(很容易)。
Mark Amery

61

我通常更喜欢异常,因为它们具有更多的上下文信息,并且可以(如果使用得当)将错误更清楚地传达给程序员。

另一方面,错误代码比异常轻巧,但难以维护。错误检查可以无意中省略。错误代码更难维护,因为您必须保留所有错误代码的目录,然后打开结果以查看抛出了什么错误。错误范围在这里可能会有所帮助,因为如果我们唯一感兴趣的是是否存在错误,则检查起来更简单(例如,大于或等于0的HRESULT错误代码表示成功,小于零就是失败)。可以无意中省略它们,因为没有程序性的强迫开发人员检查错误代码。另一方面,您不能忽略异常。

总而言之,在几乎所有情况下,我都倾向于使用异常而不是错误代码。


4
“错误代码比异常轻巧”取决于您所测量的内容以及如何进行测量。提出基于异常的API可以更快得多的测试很容易。
Mooing Duck

1
@smink,要点,但是我们如何解决异常的开销?错误代码不仅轻巧,而且基本上没有重量。异常不仅是中等重量的,它们是重量级的对象,包含堆栈信息和杂项,我们无论如何都不会使用它们。
Pacerier,2014年

3
@Pacerier:您只在考虑引发异常的测试。您100%是正确的,抛出C ++异常比返回错误代码要慢得多。放手,不用辩论。我们不同的是其他99.999%的代码。除例外情况外,我们不必检查每个语句之间的错误代码返回,从而使代码的速度提高了1-50%(或不取决于您的编译器)。这意味着完整的代码可以更快或更慢,这完全取决于代码的编写方式和引发异常的频率。
Mooing Duck 2014年

1
@Pacerier:通过我刚才编写的这个人工测试,基于异常的代码与MSVC和Clang中的错误代码一样快,尽管不是GCC:coliru.stacked-crooked.com/a/e81694e5c508945b(底部计时)。与其他编译器相比,我使用的GCC标志似乎生成了异常缓慢的异常。我有偏见,所以请批评我的测试,并随时尝试其他变体。
Mooing Duck

4
@Mooing Duck,如果您同时测试了错误代码和异常,则必须启用了异常。如果是这样,您的结果将建议使用带有异常处理开销的错误代码不会比仅使用异常慢。错误代码测试需要在禁用了异常的情况下执行才能获得有意义的结果。
Mika Haarahiltunen 2014年

24

我喜欢例外,因为

  • 他们中断了逻辑流
  • 他们受益于类层次结构,它提供了更多的功能
  • 如果使用得当,则可能表示各种错误(例如,InvalidMethodCallException也是LogicException,因为这两种情况都发生在代码中的错误应在运行时之前可检测到时),并且
  • 它们可以用于增强错误(即,FileReadException类定义可以包含用于检查文件是否存在或已被锁定等的代码)

2
您的第四点是不公平的:转换为对象时的错误状态,还可以包含用于检查文件是否存在或是否已锁定的代码,等等。这仅仅是stackoverflow.com/a/3157182/632951
Pacerier 2014年

1
“他们中断逻辑流”。异常或多或少会带来以下影响goto
peterchaula

22

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


17

毫无疑问,错误代码异常。您可以从异常中获得与使用错误代码相同的很多好处,但是在没有错误代码缺点的情况下,也可以从异常中获得很多好处。唯一的例外是,它的开销要稍微多一些。但是在当今时代,对于几乎所有应用程序来说,开销都可以忽略不计。

这里有一些文章讨论,比较和对比这两种技术:

这些链接中有一些很好的链接可以使您进一步阅读。


16

我永远不会混合使用这两种模型...当您从使用错误代码的堆栈的一部分移到使用异常的更高部分时,很难将一种模型转换为另一种模型。

异常适用于“任何阻止或禁止该方法或子例程执行您要求的操作的事件” ...请勿将有关异常情况,异常情况或系统状态等的消息传递回去。请使用返回值或ref (或out)参数。

异常允许使用依赖于方法功能的语义来编写(和使用)方法,即可以键入返回Employee对象或Employees List的方法来执行此操作,并且可以通过调用来使用它。

Employee EmpOfMonth = GetEmployeeOfTheMonth();

使用错误代码,所有方法都将返回错误代码,因此,对于那些需要返回调用代码要使用的其他内容的方法,您必须传递一个引用变量以填充该数据,并测试该变量的返回值。错误代码,并在每个函数或方法调用上进行处理。

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

如果您编写代码以使每个方法只能做一个简单的事情,那么只要该方法无法实现该方法的预期目标,就应该抛出异常。异常比错误代码更丰富,更易于使用。您的代码更加简洁-“常规”代码路径的标准流程可以严格用于这种方法能够完成您想要执行的操作的情况……然后清理或处理该代码。可以防止正常情况发生的“异常”情况导致无法成功完成该方法。此外,如果您无法处理发生异常的情况,必须将其沿堆栈向上传递到UI(或更糟糕的是,从中间层组件到UI的整个连线),则使用异常模型,


好答案!即时解决方案!
Cristian E.

11

过去,我加入了错误代码阵营(过多的C编程)。但是现在我已经看到了光明。

是的,例外会给系统带来一些负担。但是它们简化了代码,减少了错误(和WTF)的数量。

因此,请使用异常,但要明智地使用它们。他们将成为您的朋友。

作为旁注。我学会了记录哪种方法可以引发哪个异常。不幸的是,大多数语言都不需要这样做。但这增加了在正确的级别上处理正确的异常的机会。


1
yap C确实在我们所有人中留下了一些习惯;)
豪尔赫·费雷拉

11

在某些情况下,以干净,清晰,正确的方式使用异常很麻烦,但是在大多数情况下,异常是显而易见的选择。与错误代码相比,异常处理的最大好处是它更改了执行流程,这很重要,原因有两个。

发生异常时,应用程序将不再遵循其“正常”执行路径。之所以如此重要的第一个原因是,除非代码的编写者一切顺利,并且真正摆脱了糟糕的道路,否则程序将停止并且不会继续执行不可预测的事情。如果未检查错误代码,并且未针对错误的错误代码采取适当的措施,则程序将继续执行正在执行的操作,并且谁知道该操作的结果将是什么。在很多情况下,让程序执行“任何操作”可能会变得非常昂贵。考虑一个程序,该程序检索公司出售的各种金融工具的绩效信息,并将该信息传递给经纪人/批发商。如果出现问题并且程序继续运行,它将错误的绩效数据发送给经纪人和批发商。我对其他人一无所知,但我不想成为一位副总裁办公室的人,解释为什么我的守则导致该公司受到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
}

关于异常的其他一些小事情也很不错。使用一堆条件逻辑来跟踪函数中被调用的任何方法是否返回了错误代码,并且将该错误代码更高地返回是很多样板工作。实际上,很多样板可能出错。我对大多数语言的异常系统抱有更多的信心,而不是弗雷德(Fred)写的if-else-if-else陈述式的老鼠窝,我还有很多更好的事情要做与我的时间相比,代码审查说了“老鼠窝”。


8

您应该同时使用。事情是要决定何时使用每个

在某些情况下,例外是显而易见的选择

  1. 在某些情况下,您无法对错误代码执行任何操作,而只需要在调用堆栈的上级进行处理,通常只需记录错误,向用户显示内容或关闭程序即可。在这些情况下,错误代码将要求您逐级手动冒出错误代码,这显然很容易处理异常。关键是这是针对意外和无法处理的情况。

  2. 但是,对于情况1(发生意外和难以处理的事情,您只是不想记录它),异常可能会有所帮助,因为您可能会添加上下文信息。例如,如果我在较低级别的数据助手中遇到SqlException,我将希望在较低级别(我知道导致该错误的SQL命令)中捕获该错误,以便捕获该信息与其他信息一起重新抛出。请注意这里的魔术字:重新扔掉,不要吞咽异常处理的第一条规则:不要吞下异常。另外,请注意,我的内部catch不需要记录任何内容,因为外部catch将具有整个堆栈跟踪并可以记录它。

  3. 在某些情况下,您有一系列命令,如果其中任何一个失败,则应清理/处置资源(*),无论这是不可恢复的情况(应抛出)还是可恢复的情况(在这种情况下,您都可以在本地或在调用者代码中处理,但您无需例外)。显然,将所有这些命令放在一次尝试中要容易得多,而不是在每种方法之后测试错误代码,然后在finally块中进行清理/处置要容易得多。请注意,如果您希望错误冒出气泡(这可能是您想要的),则甚至不需要捕获它-您只需使用finally进行清理/处置 -如果需要,您仅应使用catch / rerow添加上下文信息(请参见项目符号2)。

    一个示例是事务块内的一系列SQL语句。同样,这也是一种“难以处理的”情况,即使您决定尽早捕获它(在本地进行处理,而不是冒顶到顶部),这仍然是一种致命的情况,在这种情况下,最好的结果是中止一切或至少中止大笔费用过程的一部分。
    (*)就像on error goto我们在旧的Visual Basic中使用的

  4. 在构造函数中,您只能抛出异常。

话虽如此,在所有其他情况下,您将返回一些信息,而调用方CAN / SHOULD会对这些信息采取行动,使用返回代码可能是更好的选择。这包括所有预期的“错误”,因为它们可能应该由直接调用者处理,并且几乎不需要在堆栈中冒出太多的层次。

当然,总是有可能将预期的错误视为异常,然后立即捕获到上一级,并且还可以将每一行代码包含在try catch中并针对每个可能的错误采取措施。IMO,这是一个糟糕的设计,不仅因为它冗长得多,而且还特别是因为如果不阅读源代码就不会抛出可能的异常-并且可以通过任何深层方法抛出异常,从而创建不可见的gotos。它们通过创建多个不可见的退出点来破坏代码结构,这些退出点使代码难以阅读和检查。换句话说,您永远不要将异常用作流程控制,因为其他人很难理解和维护。理解所有可能的测试代码流甚至会变得困难。
再次:为了正确进行清理/处置,您可以尝试使用try-finally而不抓到任何东西

关于返回码的最普遍的批评是“有人可以忽略错误码,但从某种意义上说,有人也可以吞下异常。两种方法都容易处理不良的异常。但是,编写基于错误码的良好程序仍然要容易得多。这比编写基于异常的程序要好得多,而且如果由于某种原因一个人决定忽略所有错误(旧错误on error resume next),则可以轻松地使用返回代码来做到这一点,而如果没有大量try-catchs样板,就无法做到这一点。

关于返回码的第二种最普遍的批评是“很难冒泡”-但这是因为人们不理解异常是针对不可恢复的情况的,而错误代码不是。

在异常和错误代码之间进行选择是一个灰色区域。甚至有可能需要从某种可重用的业务方法中获取错误代码,然后决定将其包装到异常中(可能添加信息)并使其冒泡。但是,假设所有错误都应作为异常抛出是一个设计错误。

把它们加起来:

  • 当我遇到意想不到的情况时,我喜欢使用异常,这种情况下没有太多事情要做,通常我们希望中止一大堆代码甚至整个操作或程序。这就像旧的“ on error goto”。

  • 当我期望调用者代码可以/应该采取某些措施时,我喜欢使用返回代码。这包括大多数业务方法,API,验证等。

异常代码与错误代码之间的差异是GO语言的设计原则之一,它对致命的意外情况使用“恐慌”,而正常的预期情况则作为错误返回。

关于GO,它还允许多个返回值,这对使用返回码有很大帮助,因为您可以同时返回错误和其他信息。在C#/ Java上,我们可以通过不带参数的Tuples或(我最喜欢的)泛型来实现,将它们与枚举结合起来可以为调用者提供清晰的错误代码:

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仍然是不好的设计。


4

我的理由是,如果您正在编写确实需要性能的低级驱动程序,请使用错误代码。但是,如果您在更高级别的应用程序中使用该代码,并且它可以处理一些开销,那么请使用检查这些错误代码并引发异常的接口包装该代码。

在所有其他情况下,例外可能是解决之道。


4

我可能坐在这里的篱笆上,但是...

  1. 这取决于语言。
  2. 无论选择哪种模型,都应保持一致的使用方式。

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

在C ++中(至少在STL中),通常仅针对真正的异常错误抛出异常(我自己几乎从未见过异常)。我认为没有理由在自己的代码中做任何不同的事情。是的,忽略返回值很容易,但是C ++也不强迫您捕获异常。我认为您只需要养成这样做的习惯即可。

我使用的代码库大部分是C ++,我们几乎在所有地方都使用错误代码,但是有一个模块会引发任何错误的异常,包括非常异常的异常,并且使用该模块的所有代码都非常糟糕。但这可能只是因为我们混合了异常和错误代码。始终使用错误代码的代码更容易使用。如果我们的代码始终使用异常,那么可能不会那么糟糕。混合这两者似乎效果不佳。


4

由于我使用C ++,并且使用RAII可以安全地使用它们,因此几乎只使用异常。它将错误处理从正常程序流中拉出来,并使意图更加清晰。

但是,在特殊情况下,我确实会保留例外。如果我期望某个错误会发生很多,我将在执行该操作之前检查该操作是否会成功,或者调用使用错误代码的函数版本(例如TryParse()


3

方法签名应向您传达方法的作用。如long errorCode = getErrorCode();之类的东西 可能没问题,但是长的errorCode = fetchRecord(); 令人困惑。


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;

}

这样,我就可以使用“例外”来定义类别错误,并使用错误代码来定义有关该问题的更多详细信息。


2
嗯... throw new DataValidationException("The input is too small")?例外的优点之一是允许提供详细信息。
伊娃(Eva)2013年

2

例外是例外情况下发生的-即,当不在代码的正常流程中时。

混合使用异常和错误代码是完全合法的,其中错误代码代表某种状态,而不是代码本身的运行错误(例如,检查子进程的返回代码)。

但是,当发生特殊情况时,我相信例外是最具表现力的模型。

在某些情况下,您可能希望或不得不使用错误代码代替Exception,并且已经充分地涵盖了这些错误代码(除了诸如编译器支持之类的其他明显限制之外)。

但是从另一个方向来看,使用异常可以使您为错误处理构建甚至更高层次的抽象,这可以使您的代码更具表现力和自然性。我强烈建议阅读C ++专家Andrei Alexandrescu撰写的这篇出色的文章,但被低估了,他称之为“执法”:http//www.ddj.com/cpp/184403864。尽管这是有关C ++的文章,但这些原则通常适用,并且我已经非常成功地将强制执行概念转换为C#。


2

首先,我同意Tom的回答,只要不是面向服务的体系结构(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
    };
}

如果在您的服务响应中一致地使用这些功能,它将创建一个很好的模式来处理应用程序中的成功/失败。这样可以简化服务内部以及服务之间的异步调用中的错误处理。


1

对于所有错误情况,我都希望使用“异常”,除非失败是返回原始数据类型的函数的预期无错误结果。例如,如果找不到较大字符串中的子字符串索引,通常会返回-1,而不是引发NotFoundException。

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

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

这里要记住的一件事是API随时间的演变。良好的API可以以多种方式更改和扩展故障行为,而不会破坏客户端。例如,如果客户端错误句柄检查了4种错误情况,并且您向函数添加了第五个错误值,则客户端处理程序可能不会对此进行测试并中断。如果引发异常,通常可以使客户端更轻松地迁移到库的较新版本。

另一个要考虑的问题是在团队中工作时,在这里为所有开发人员划清界限以做出这样的决定。例如,“高级内容的异常,低级内容的错误代码”是非常主观的。

在任何情况下,如果可能有多种错误,源代码都决不能使用数字文字来返回错误代码或进行处理(如果x == -7 ...,则返回-7),但是始终是一个命名常量(如果x == NO_SUCH_FOO,则返回NO_SUCH_FOO)。


1

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

例如,您决定仅使用异常。但是一旦您决定使用异步事件处理。在这种情况下,使用异常进行错误处理是个坏主意。但是在应用程序中到处都使用错误代码是乏味的。

因此,我认为同时使用异常和错误代码是正常的。


0

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


0

我认为这还取决于您是否真的需要从结果中获取诸如堆栈跟踪之类的信息。如果是,您肯定会选择Exception,它为对象提供了很多有关问题的信息。但是,如果您只对结果感兴趣,而不关心为什么要返回结果,请输入错误代码。

例如,当您处理文件并遇到IOException时,客户端可能会想知道从何处触发,打开文件或解析文件等。因此最好返回IOException或其特定的子类。但是,如果您有登录方法,并且想知道它是否成功,则返回布尔值或显示正确的消息,然后返回错误代码。在这里,客户不希望知道逻辑的哪一部分导致了该错误代码。他只知道其凭据无效或帐户锁定等。

我能想到的另一个用例是数据何时在网络上传输。您的远程方法可以仅返回错误代码而不是Exception,以最大程度地减少数据传输。


0

我的一般规则是:

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

-1

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


3
不。请参见win32 GetLastError()范例。我没有为之辩护,只是说出你是错误的。
蒂姆(Tim)

4
实际上,还有许多其他方法可以做到这一点。另一种方法是返回包含错误代码和实际返回值的对象。还有另一种方法是进行引用传递。
Pacerier,2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.