首页 文章

使用断言或例外通过 Contract 设计? [关闭]

提问于
浏览
116

在按 Contract 进行编程时,在开始履行职责之前,函数或方法首先检查其先决条件是否得到满足,对吗?执行这些检查的两个最突出的方法是 assertexception .

  • 断言仅在调试模式下失败 . 确保(单元)测试所有单独的 Contract 前提条件以确定它们是否确实失败是至关重要的 .
    调试和释放模式下
  • 异常失败 . 这样做的好处是,经过测试的调试行为与发布行为相同,但它会导致运行时性能下降 .

你认为哪一个更好?

见相关问题here

14 回答

  • 5

    在发布版本中禁用断言就像是说“我将永远不会在发布版本中出现任何问题”,而这通常不是这种情况 . 因此,不应在发布版本中禁用assert . 但是,您不希望发生错误时发布版本崩溃,是吗?

    因此,请使用异常并使用它们 . 使用一个好的,可靠的异常层次结构并确保捕获并且可以在调试器中抛出异常抛出以捕获它,并且在释放模式下,您可以补偿错误而不是直接崩溃 . 这是更安全的方式 .

  • 2

    经验法则是,当您尝试捕获自己的错误时应使用断言,并在尝试捕获其他人的错误时使用异常 . 换句话说,您应该使用异常来检查公共API函数的前提条件,以及何时获得系统外部的任何数据 . 您应该将断言用于系统内部的功能或数据 .

  • 2

    我遵循的原则是:如果通过编码可以现实地避免情况,那么使用断言 . 否则使用例外 .

    断言是为了确保遵守 Contract . Contract 必须公平,以便客户必须能够确保其符合要求 . 例如,您可以在 Contract 中声明URL必须有效,因为有关什么是有效URL的规则是已知且一致的 .

    例外情况适用于客户端和服务器无法控制的情况 . 一个例外意味着某些事情出了问题,并且没有任何事情可以避免它 . 例如,网络连接在应用程序控制之外,因此无法避免网络错误 .

    我想补充一点,Assertion / Exception的区别并不是考虑它的最佳方式 . 你真正想要考虑的是 Contract 以及如何实施 Contract . 在我上面的URL示例中,最好的办法是使用一个封装URL的类,它是Null或有效的URL . 它是将字符串转换为强制执行 Contract 的URL,如果无效则抛出异常 . 具有URL参数的方法比具有String参数的方法和指定URL的断言更清晰 .

  • 0

    断言是为了捕捉开发人员做错的事情(不仅仅是你自己 - 团队中的另一个开发人员) . 如果用户错误可以创建此条件是合理的,那么它应该是一个例外 .

    同样考虑后果 . 断言通常会关闭应用程序 . 如果有任何现实的期望可以从中恢复条件,则应该使用异常 .

    另一方面,如果问题可能是程序员错误导致的,那么请使用断言,因为您希望尽快了解它 . 可能会捕获并处理异常,您永远不会发现它 . 是的,您应该在发布代码中禁用断言,因为如果有最轻微的可能性,您希望应用程序恢复 . 即使您的程序状态被严重破坏,用户也可能能够保存他们的工作 .

  • 0

    “断言仅在调试模式下失败”并不完全正确 .

    在Bertrand Meyer的第2版“面向对象软件构建”中,作者留下了一扇门,用于检查发布模式中的前提条件 . 在这种情况下,当断言失败时会发生什么......提出了一个断言违例异常!在这种情况下,没有从情况中恢复:虽然可以做一些有用的事情,它是自动生成错误报告,在某些情况下,重新启动应用程序 .

    这背后的动机是前提条件通常比不变量和后置条件更便宜,并且在某些情况下,正确性和发布版本中的"safety"比速度更重要 . 即对于许多应用程序而言,速度不是问题,而是 robustness (程序在其行为不正确时,即在 Contract 被破坏时以安全的方式行为的能力)是 .

    您是否应始终启用前置条件检查?这取决于 . 由你决定 . 没有普遍性回答 . 如果您正在为银行制作软件,最好使用警报消息中断执行,而不是转移1,000,000美元而不是1,000美元 . 但是,如果你正在编程游戏呢?也许你需要你可以获得的所有速度,如果有人得到1000分而不是10分,因为前提条件没有捕获的错误(因为它们没有启用),运气不好 .

    在这两种情况下,理想情况下,您应该在测试期间捕获该错误,并且应该在启用断言的情况下执行大部分测试 . 这里讨论的是对于那些由于不完整的测试而未在早期检测到的情况下 生产环境 代码中的前提条件失败的罕见情况的最佳策略是什么 .

    总而言之, you can have assertions and still get the exceptions automatically ,如果你让它们启用 - 至少在埃菲尔 . 我想在C中做同样的事情,你需要自己输入 .

    另见:When should assertions stay in production code?

  • 22

    关于在comp.lang.c .moderated上的发布版本中启用/禁用断言有一个巨大的thread,如果你有几个星期,你可以看到对此的看法有多么不同 . :)

    coppro相反,我相信如果您不确定在发布版本中是否可以禁用断言,那么它不应该是一个断言 . 断言是为了防止程序不变量被破坏 . 在这种情况下,就您的代码的客户而言,将会有两种可能的结果之一:

    • 因某种操作系统类型故障而死亡,导致调用中止 . (没有断言)

    • 通过直接调用中止死亡 . (有断言)

    用户没有区别,但是,断言可能会在代码中出现代码不会失败的绝大多数运行中增加不必要的性能成本 .

    这个问题的答案实际上更多地取决于API的客户是谁 . 如果您正在编写提供API的库,那么您需要某种形式的机制来通知您的客户他们错误地使用了API . 除非你提供两个版本的库(一个带有断言,一个没有断言),否则断言是不太可能的合适选择 .

    然而,就个人而言,我不确定我是否会对这种情况采取例外 . 例外更适合可以进行适当形式的恢复的地方 . 例如,您可能正在尝试分配内存 . 当你捕获'std :: bad_alloc'异常时,可能会释放内存并重试 .

  • 0

    我概述了我对此事的状况的看法:How do you validate an object's internal state? . 一般来说,主张您的主张并抛弃其他人的违规行为 . 要在发布版本中禁用断言,您可以执行以下操作:

    • 禁用断言以进行昂贵的检查(例如检查是否订购了范围)

    • 启用简单检查(比如检查空指针或布尔值)

    当然,在发布版本中,失败的断言和未捕获的异常应该以与调试版本相同的方式处理(它可以只调用std :: abort) . 在某处写入错误日志(可能写入文件),告诉客户发生了内部错误 . 客户将能够向您发送日志文件 .

  • 192

    你问的是设计时和运行时错误之间的区别 .

    断言是'嘿程序员,这是破坏'的通知,他们在那里提醒你在他们发生时你不会注意到的错误 .

    例外情况是“嘿用户,有些事情出错”通知(显然你可以编写代码来捕获它们以便用户永远不会被告知)但是这些设计是在Joe用户使用应用程序时在运行时发生的 .

    因此,如果您认为可以解决所有问题,请仅使用例外 . 如果你认为你不能......使用例外 . 您仍然可以使用调试断言来减少异常数量 .

    不要忘记,许多前提条件都是用户提供的数据,因此您需要一种很好的方式来通知用户他的数据不好 . 为此,您通常需要将调用堆栈中的错误数据返回到与其交互的位 . 如果您的应用程序是n层,那么断言就没用了 - 加倍 .

    最后,我不使用 - 错误代码远远优于您认为会经常发生的错误 . :)

  • 1

    我更喜欢第二个 . 虽然您的测试可能运行良好,但Murphy表示意外情况会出错 . 因此,不是在实际的错误方法调用中获得异常,而是最终会追溯到更深层次的NullPointerException(或等效的)10个堆栈帧 .

  • 0

    以前的答案是正确的:对公共API函数使用异常 . 您可能希望弯曲此规则的唯一时间是检查计算成本高昂 . 在这种情况下,你 can 把它放在断言中 .

    如果您认为可能违反该前提条件,请将其作为例外,或重构前提条件 .

  • 0

    你应该使用两者 . 断言是为了方便您作为开发人员 . 例外可以捕获您错过或未错过的内容在运行时期望 .

    我已经喜欢glib's error reporting functions而不是普通的断言 . 它们的行为类似于断言语句,但它们不是暂停程序,而是返回一个值并让程序继续运行 . 它的效果非常好,作为奖励,当函数没有返回"what it's supposed to"时,您可以看到程序其余部分会发生什么 . 如果它崩溃了,你知道你的错误检查在其他地方不严格 .

    在我的上一个项目中,我使用这些函数来实现前置条件检查,如果其中一个失败,我会将堆栈跟踪打印到日志文件但继续运行 . 当其他人在运行我的调试版本时遇到问题时,节省了大量的调试时间 .

    #ifdef DEBUG
    #define RETURN_IF_FAIL(expr)      do {                      \
     if (!(expr))                                           \
     {                                                      \
         fprintf(stderr,                                        \
            "file %s: line %d (%s): precondition `%s' failed.", \
            __FILE__,                                           \
            __LINE__,                                           \
            __PRETTY_FUNCTION__,                                \
            #expr);                                             \
         ::print_stack_trace(2);                                \
         return;                                                \
     };               } while(0)
    #define RETURN_VAL_IF_FAIL(expr, val)  do {                         \
     if (!(expr))                                                   \
     {                                                              \
        fprintf(stderr,                                             \
            "file %s: line %d (%s): precondition `%s' failed.",     \
            __FILE__,                                               \
            __LINE__,                                               \
            __PRETTY_FUNCTION__,                                    \
            #expr);                                                 \
         ::print_stack_trace(2);                                    \
         return val;                                                \
     };               } while(0)
    #else
    #define RETURN_IF_FAIL(expr)
    #define RETURN_VAL_IF_FAIL(expr, val)
    #endif
    

    如果我需要运行时检查参数,我会这样做:

    char *doSomething(char *ptr)
    {
        RETURN_VAL_IF_FAIL(ptr != NULL, NULL);  // same as assert(ptr != NULL), but returns NULL if it fails.
                                                // Goes away when debug off.
    
        if( ptr != NULL )
        {
           ...
        }
    
        return ptr;
    }
    
  • 6

    我尝试用自己的观点综合其他几个答案 .

    对于你想在 生产环境 中禁用它的情况使用断言,错误地让它们进入 . 在 生产环境 中禁用而不是在开发中禁用的唯一真正原因是加速程序 . 在大多数情况下,这种加速速度并不显着,但有时代码是时间关键的,或者测试计算成本很高 . 如果代码是关键任务,那么尽管速度减慢,但异常可能是最好的 .

    如果有任何真正的恢复机会,请使用异常,因为断言不是为了从中恢复 . 例如,代码很少设计用于从编程错误中恢复,但它旨在从网络故障或锁定文件等因素中恢复 . 错误不应仅仅因为不受程序员控制而作为例外处理 . 相反,与编码错误相比,这些错误的可预测性使得它们更容易恢复 .

    重新论证调试断言更容易:来自正确命名的异常的堆栈跟踪与断言一样容易阅读 . 好的代码应该只捕获特定类型的异常,因此异常不会因为被捕获而被忽视 . 但是,我认为Java有时会迫使您捕获所有异常 .

  • 0

    另见this question

    在某些情况下,在构建发布时,断言被禁用 . 您可能无法控制它(否则,您可以使用断言构建),因此这样做可能是个好主意 . “纠正”输入值的问题在于调用者无法获得他们期望的内容,这可能导致程序的完全不同部分出现问题甚至崩溃,从而使调试成为一场噩梦 . 我通常在if语句中抛出一个异常来接管断言的作用,以防它们被禁用assert(value> 0);
    if(value <= 0)抛出新的ArgumentOutOfRangeException(“value”);
    //做东西

  • 40

    对我而言,经验法则是使用断言表达式来查找外部错误的内部错误和异常 . 您可以从Greg的here的以下讨论中获益良多 .

    断言表达式用于查找编程错误:程序逻辑本身中的错误或其相应实现中的错误 . 断言条件验证程序是否保持定义状态 . “已定义的状态”基本上与计划的假设一致 . 注意,程序的“已定义状态”不一定是“理想状态”,甚至不是“通常状态”,甚至不是“有用状态”,而是后来更重要的一点 . 要理解断言如何适合程序,请考虑C程序中即将取消引用指针的例程 . 现在应该例程测试在解除引用之前指针是否为NULL,还是应该断言指针不是NULL然后继续并取消引用它?我想大多数开发人员都希望同时执行这两个操作,添加断言,还要检查指针是否有NULL值,以便在断言条件失败时不会崩溃 . 从表面上看,执行测试和检查似乎是最明智的决定 . 与其断言条件不同,程序的错误处理(异常)不是指程序中的错误,而是指程序从其环境中获得的输入 . 这些通常是某人的“错误”,例如用户尝试在不输入密码的情况下登录帐户 . 即使错误可能阻止程序任务的成功完成,也没有程序失败 . 由于外部错误,程序无法在没有密码的情况下登录用户 - 用户的部分出错 . 如果情况不同,用户输入正确的密码,程序无法识别;然后虽然结果仍然相同,但失败现在属于该计划 . 错误处理(例外)的目的有两个 . 第一种是与用户(或其他一些客户端)通信,检测到程序输入中的错误及其含义 . 第二个目标是在检测到错误后将应用程序恢复到定义良好的状态 . 请注意程序本身在这种情况下没有错误 . 当然,程序可能处于非理想状态,甚至处于无用的状态,但没有编程错误 . 相反,由于错误恢复状态是程序设计所预期的状态,因此它是程序可以处理的状态 .

    PS:你可能想看看类似的问题:Exception Vs Assertion .

相关问题