代码之家  ›  专栏  ›  技术社区  ›  Jorge Ferreira

例外或错误代码的约定

  •  96
  • Jorge Ferreira  · 技术社区  · 17 年前

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

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

    22 回复  |  直到 7 年前
        1
  •  88
  •   Tom Dunham    17 年前

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

    但是,如果我正在编写一段代码 一定知道 在任何可能的情况下,我都需要错误代码。否则,我必须知道函数中每一行可以抛出的每个异常,才能知道它将做什么(读取 The Exception That Grounded an Airline 了解这有多棘手)。编写对各种情况(包括不愉快的情况)做出适当反应的代码既单调又困难,但这是因为编写无错误代码既单调又困难,而不是因为您正在传递错误代码。

    Raymond Chen and Joel

        2
  •  65
  •   Fuhrmanator    9 年前

    我通常更喜欢异常,因为它们有更多的上下文信息,并且可以(在正确使用时)以更清晰的方式将错误传递给程序员。

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

    总而言之,在几乎所有情况下,我更喜欢异常而不是错误代码。

        3
  •  24
  •   JamShady    17 年前

    • 他们打断了逻辑的流动
    • 如果使用得当,可能会出现各种各样的错误(例如,InvalidMethodCallException也是一种逻辑异常,因为当您的代码中存在一个应该在运行前检测到的错误时,这两种情况都会发生),以及
    • 它们可用于增强错误(即FileReadException类定义可包含代码,以检查文件是否存在或是否已锁定等)
        4
  •  23
  •   Maxam    17 年前

        5
  •  17
  •   Your Common Sense    17 年前

    有一些很好的链接可以让你进一步阅读。

        6
  •  17
  •   Charles Bretana    12 年前

    例外情况是“任何停止或禁止方法或子例程执行您要求它执行的操作的事情”。。。不要传回有关不规则或异常情况或系统状态等的消息。使用返回值或ref(或out)参数。

    异常允许使用依赖于方法函数的语义编写(和使用)方法,即,可以键入返回Employee对象或员工列表的方法来执行此操作,您可以通过调用来利用它。

    Employee EmpOfMonth = GetEmployeeOfTheMonth();
    

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

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

        7
  •  14
  •   drizin    5 年前

    你应该两者都用。问题是决定何时使用每一个 .

    :

    1. 在某些情况下 您不能对错误代码执行任何操作 需要在调用堆栈的上层处理它 意外的和不可处理的

    2. 添加上下文信息 . 例如,如果我在较低级别的数据帮助程序中得到一个SqlException,我希望在较低级别捕获该错误(我知道导致错误的SQL命令),以便捕获该信息 并重新催促 提供更多信息。请注意这里的神奇词语: 重新吸,不要吞咽 The first rule of exception handling: do not swallow exceptions . 另外,请注意,我的内部捕获不需要记录任何内容,因为外部捕获将具有整个堆栈跟踪,并且可能会记录它。

    3. 在某些情况下,您有一系列命令,并且 你应该 (*),无论这是不可恢复的情况(应该抛出)还是可恢复的情况(在这种情况下,您可以在本地或调用方代码中处理,但不需要异常)。显然,在一次尝试中使用所有这些命令要容易得多,而不是在每个方法之后测试错误代码,并在finally块中进行清理/处置。请注意 如果您希望错误冒泡(这可能是您想要的),您甚至不需要捕获它-您只需使用finally进行清理/处置

      致命情况
      (*)这就像 on error goto 我们在旧的VisualBasic中使用的

    话虽如此,在所有其他情况下,您返回的信息 ,因为它们可能应该由直接调用方处理,并且几乎不需要在堆栈中向上冒泡太多级别。

    当然,总是可以将预期的错误视为异常,然后立即捕获上面的一个级别,也可以在try-catch中包含每一行代码,并对每一个可能的错误采取措施。在我看来,这是一个糟糕的设计,不仅因为它要详细得多,而且特别是因为在没有阅读源代码的情况下可能抛出的异常并不明显,而且异常可以从任何深层方法抛出,创建 invisible gotos . 它们通过创建多个不可见的出口点来打破代码结构,使代码难以读取和检查。换句话说,你不应该使用 flow-control ,因为其他人很难理解和维护。理解所有可能用于测试的代码流可能会变得更加困难。
    再一次:

    对返回码最流行的批评是“有人可以忽略错误码,但在同样意义上,也有人可以接受异常。 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 . 如果出于任何原因决定忽略所有错误(旧的 on error resume next ),您可以使用返回代码轻松地完成此操作,如果没有大量的try-catch样板文件,则无法完成此操作。

    判断异常和错误代码是一个灰色区域。您甚至可能需要从一些可重用的业务方法中获取错误代码,然后决定将其包装为异常(可能会添加信息)并让其冒泡。但是,假设所有错误都应该作为异常抛出是一个设计错误。

    总而言之:

    • 我喜欢在遇到意外情况时使用异常,在这种情况下,没有太多事情要做,通常我们想要中止一大块代码,甚至整个操作或程序。这就像旧的“错误转到”。

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

    然而关于GO,它也允许 multiple return values

    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语句中覆盖了该新值。除了例外,你真的不能这么做。当您使用返回码时,您通常会提前知道所有可能的错误,并对它们进行测试。除了例外,你通常不知道会发生什么。将枚举包装到异常中(而不是泛型)是一种替代方法(只要清楚每个方法将抛出的异常类型),但在我看来,这仍然是一种糟糕的设计。

    编辑2020-10-11 :

    new Tuples syntax 它允许多个返回值(因此我们可以使用GO-like语法,其中方法返回结果或错误)。

    
    public enum CreateUserResultCodeEnum
    {
        [Description("Username not available")]
        NOT_AVAILABLE,
    }
    
    public (User user, CreateUserResultCodeEnum? error) CreateUser(string userName)
        // (try to create user, check if not available...)
        if (notAvailable)
            return (null, CreateUserResultCodeEnum.NOT_AVAILABLE);
        return (user, null);
    }
    
    // How to call and deconstruct tuple:
    (var user, var error) = CreateUser("john.doe");
    if (user != null) ...
    if (error == CreateUserResultCodeEnum.NOT_AVAILABLE) ...
    
    // Or returning a single object (named tuple):
    var result = CreateUser("john.doe");
    if (result.user != null) ...
    if (result.error == CreateUserResultCodeEnum.NOT_AVAILABLE) ...
    

    编辑2021-01-09

    几天前 I wrote this blog post 关于我们如何(在某些情况下!)使用多个返回而不是异常(如上面解释的golang约定,不应该替换所有异常,而是应该让您在何时使用异常和何时使用返回代码之间做出决定)。 在文章的最后,我混合了两种模式——基本上我使用的是 作为基础结构。 基本上我用 隐式转换运算符 在…之间来回转换 ValueTuple CommandResult<TEntity, TError> .

        8
  •  12
  •   Dogs    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语句的信心大得多,而且我花时间做的事情要比代码审查更好。

        9
  •  11
  •   Toon Krijthe    17 年前

    是的,例外情况对系统来说是一个负担。但它们简化了代码,减少了错误(和WTF)的数量。

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

        10
  •  4
  •   Claudiu    17 年前

    在所有其他情况下,例外情况可能是一条出路。

        11
  •  4
  •   sakana    17 年前

    我的方法是,我们可以同时使用这两种代码,即异常代码和错误代码。

    我用来定义几种类型的异常(例如: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;
    
    }
    

        12
  •  4
  •   jrb    17 年前

    1. 这取决于语言。
    2. 无论你选择哪种型号,在使用上都要保持一致。

    在Python中,使用异常是标准实践,我很乐意定义自己的异常。在C语言中,你根本没有例外。

    在C++中(至少在STL中),异常通常只针对真正异常的错误而抛出(实际上我自己从来没有见过)。我认为没有理由在我自己的代码中做任何不同的事情。是的,忽略返回值很容易,但是C++也不会强迫你捕获异常。我认为你必须养成这样做的习惯。

    我工作的代码库主要是C++,我们几乎到处都使用错误代码,但是有一个模块会为任何错误增加异常,包括非常不寻常的错误,使用该模块的所有代码都非常可怕。但这可能只是因为我们混合了异常和错误代码。一贯使用错误代码的代码更易于使用。如果我们的代码一直使用异常,也许就不会那么糟糕了。把两者混合起来似乎效果不太好。

        13
  •  4
  •   Salman A    17 年前

    由于我使用C++,并且RAII使它们安全使用,所以我几乎完全使用异常。它将错误处理从正常的程序流中拉出来,使意图更加明确。

    TryParse() )

        14
  •  3
  •   Paul Croarkin    17 年前

    long errorCode=getErrorCode(); 也许可以,但是

        15
  •  2
  •   philsquared    17 年前

    例外情况是 环境-即,当它们不是代码正常流程的一部分时。

    但当发生异常情况时,我相信异常是最具表现力的模型。

    在某些情况下,您可能更喜欢或不得不使用错误代码来代替异常,并且这些情况已经得到了充分的介绍(除了其他明显的限制,例如编译器支持)。

    http://www.ddj.com/cpp/184403864 . 虽然它是C++文章,但原则是普遍适用的,我成功地把这个概念翻译成C。

        16
  •  2
  •   Community Mohan Dere    9 年前

    首先,我同意汤姆的观点 answer 高级人员使用异常,低级人员使用错误代码,只要它不是面向服务的体系结构(SOA)。

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

    当这些在您的服务响应中一致使用时,它会创建一个非常好的模式来处理应用程序中的成功/失败。这使得在服务内部以及跨服务的异步调用中更容易处理错误。

        17
  •  1
  •   tkruse    12 年前

    我更喜欢所有错误情况下的异常,除非失败是返回原始数据类型的函数的预期无bug结果。例如,在较大字符串中查找子字符串的索引,如果未找到,通常会返回-1,而不是引发NotFoundException。

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

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

    另一件需要考虑的事情是在团队中工作,为所有开发人员划清界限做出这样的决定。例如,“高级人员例外,低级人员错误代码”是非常主观的。

        18
  •  1
  •   gomons    12 年前

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

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

        19
  •  0
  •   Jim C    17 年前

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

        20
  •  0
  •   ashah    10 年前

    我认为这还取决于您是否真的需要来自结果的堆栈跟踪之类的信息。如果是的话,您肯定会选择Exception,它为对象提供了关于问题的大量信息。然而,若你们只是对结果感兴趣,而不关心为什么会有这样的结果,那个么就去寻找错误代码。

    我能想到的另一个用例是数据在网络上传输。您的远程方法可以只返回错误代码而不是异常,以最小化数据传输。

        21
  •  0
  •   DEADBEEF    9 年前

    我的一般规则是:

        22
  •  -1
  •   Omar Kooheji    17 年前

    推荐文章