使用断言或异常进行契约式设计?

当通过契约编程一个函数或方法时,在开始履行其职责之前,首先要检查它的前提条件是否得到满足,对吗?进行这些检查的两种最突出的方法是 assertexception

  1. 断言仅在调试模式下失败。确保(单元)测试所有单独的契约前提条件是至关重要的,以查看它们是否实际失败。
  2. 异常在调试和释放模式下失败。这样做的好处是,测试的调试行为与发布行为相同,但是它会带来运行时性能损失。

你觉得哪个更好?

见相关问题 给你

26265 次浏览

在发布版本中禁用断言就像是说“在发布版本中我将永远不会有任何问题”,事实往往并非如此。因此,在发布版本中不应该禁用断言。但是您也不希望发布版本在出现错误时崩溃,对吗?

因此,要好好使用异常。使用一个良好的、可靠的异常层次结构,确保捕获异常,并且可以在调试器中抛出异常时放置一个钩子来捕获异常,在发布模式下,可以对错误进行补偿,而不是直接崩溃。这样更安全。

您询问的是设计时错误和运行时错误之间的区别。

断言是“嘿,程序员,这是坏的”通知,它们在那里提醒你的错误,你不会注意到当它们发生时。

例外情况是“ hey user,something went wrong”通知(显然你可以编写代码来捕捉它们,这样用户就永远不会知道) ,但是这些通知是为 Joe user 在使用应用程序时发生而设计的。

因此,如果您认为可以排除所有 bug,那么只使用异常。如果你觉得你不能..。使用异常。当然,您仍然可以使用调试断言来减少异常的数量。

不要忘记,许多先决条件将是用户提供的数据,因此您需要一种好的方法来告诉用户他的数据不好。为此,通常需要将错误数据从调用堆栈返回到与其交互的位。如果你的应用程序是 n 层的,那么断言就不再有用了。

最后,我会使用非错误代码-对于您认为会经常发生的错误,错误代码要优越得多。 :)

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

我更喜欢第二个。虽然您的测试可能运行得很好,但是 摩菲表示某些意想不到的事情会出错。因此,不是在实际的错误方法调用中获得异常,而是更深地跟踪一个 NullPointerException (或等效的)10个堆栈帧。

我遵循的原则是: 如果通过编码可以实际避免某种情况,那么就使用断言。否则使用异常。

断言是为了确保合同得到遵守。合同必须是公平的,因此客户必须能够确保合同得到遵守。例如,您可以在合同中声明 URL 必须是有效的,因为关于什么是有效的 URL 和什么不是有效的 URL 的规则是已知的和一致的。

客户端和服务器都无法控制的情况是例外。例外意味着某些事情出错了,没有什么可以避免它。例如,网络连接超出了应用程序的控制范围,因此无法避免网络错误。

我想补充一点,断言/异常的区别并不是考虑它的最佳方式。你真正想考虑的是合同以及如何执行合同。在我上面的 URL 示例中,最好使用一个封装 URL 的类,它可以是 Null 或者有效的 URL。它是将一个字符串转换为一个强制执行该约定的 URL,如果异常无效,则引发异常。具有 URL 参数的方法要比具有 String 参数和指定 URL 的断言的方法清楚得多。

另见 这个问题:

我有些情况下,断言是禁用的时候,建设释放。你可以 对此没有控制权(否则,您可以使用断言进行构建 所以这样做可能是个好主意。

“修正”输入值的问题在于调用方会 没有得到他们所期望的,这可能会导致问题,甚至 在程序的完全不同的部分崩溃,使调试成为一个 噩梦。

我通常在 if 语句中引发异常来接管角色 在断言被禁用的情况下,断言的

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff

前面的答案是正确的: 对公共 API 函数使用异常。您可能希望改变此规则的唯一时间是在支票计算成本较高的时候。在这种情况下,可以将它放入一个断言中。

如果您认为可能会违反该先决条件,那么将其作为异常保留,或者将该先决条件重构掉。

你应该两个都用。断言是为了您作为开发人员的方便。异常捕获在运行时期间错过或没有预料到的内容。

我越来越喜欢 Glib 的错误报告函数而不是单纯的陈旧的断言。它们的行为类似于断言语句,但不会停止程序,只是返回一个值,让程序继续运行。它的工作效果出人意料的好,作为奖励,您可以看到当函数不返回“它应该返回的内容”时,程序的其余部分会发生什么情况。如果它崩溃了,您就知道您的错误检查在其他地方是松懈的。

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

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

断言用于捕获开发人员做错的事情(不仅仅是您自己,还包括您团队中的另一个开发人员)。如果用户错误可能创建此条件是合理的,那么它应该是一个异常。

同样地,也要考虑后果。断言通常会关闭应用程序。如果存在可以从中恢复条件的任何实际期望,则可能应该使用异常。

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

在基于 comp.lang.c + + 的版本构建中,有一个关于启用/禁用断言的大型 线。节制,如果你有几个星期,你可以看到这是多么不同的意见。:)

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

  1. 死于某种操作系统类型的故障,导致调用终止。(没有断言)
  2. 通过直接调用终止死亡。(使用断言)

对于用户来说没有区别,但是,断言可能会在代码中增加不必要的性能成本,这种性能成本存在于绝大多数运行中,代码在这些运行中不会失败。

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

然而,就我个人而言,我也不确定我会为这个案子破例。异常更适合于可以进行适当形式的恢复的地方。例如,它可能是您试图分配内存。当捕捉到“ std: : bad _ alloc”异常时,可能会释放内存并重试。

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

在 Bertrand Meyer 的 面向对象软件构造,第2版中,作者为在发布模式中检查前提条件打开了一扇门。在这种情况下,当断言失败时会发生... ... 引发断言违反异常!在这种情况下,无法从这种情况中恢复: 但是可以做一些有用的事情,即自动生成一个错误报告,并且在某些情况下重新启动应用程序。

这背后的动机是,测试前置条件通常比不变量和后置条件更便宜,在某些情况下,版本构建中的正确性和“安全性”比速度更重要。例如,对于许多应用程序来说,速度不是问题,但是 稳健性(当程序的行为不正确时,比如当契约被打破时,程序能够以安全的方式运行)是问题。

是否应该始终启用前置条件检查?看情况。你自己决定吧。没有普遍的答案。如果你正在为一家银行开发软件,用一条警告信息中断执行可能比转移100万美元而不是1000美元更好。但如果你正在编写一个游戏呢?也许你需要所有你能得到的速度,如果有人得到1000分而不是10分,因为一个错误的前提条件没有捕捉(因为他们没有启用) ,运气不好。

在这两种情况下,理想情况下,您都应该在测试期间捕捉到这个 bug,并且应该在启用断言的情况下执行大部分测试。这里讨论的是对于那些前置条件在生产代码中由于不完全测试而在早期未检测到的场景中失败的罕见情况,什么是最佳策略。

总而言之,如果启用 您可以拥有断言,但仍然可以自动获得异常,至少在埃菲尔铁路是这样的。我认为要在 C + + 中做同样的事情,你需要自己输入它。

参见: 断言什么时候应该留在生产代码中?

我在这里概述了我对事态的看法: 如何验证一个对象的内部状态?。一般来说,坚持你的主张,并抛出其他违反。对于在发布版本中禁用断言,您可以这样做:

  • 为昂贵的检查禁用断言(比如检查范围是否有序)
  • 保持启用普通检查(如检查空指针或布尔值)

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

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

对于希望在生产环境中禁用断言的情况,使用断言会导致错误地将断言保留在生产环境中。在生产中而不是在开发中禁用的唯一真正原因是为了加快程序的速度。在大多数情况下,这种加速并不显著,但是有时代码是时间关键的,或者测试的计算成本很高。如果代码是关键任务,那么异常可能是最好的,尽管缓慢。

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

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

对我来说,经验法则是使用断言表达式来查找内部错误和外部错误的异常。您可以从以下来自 给你的 Greg 的讨论中获益匪浅。

断言表达式用于查找编程错误: 程序本身逻辑中的错误或相应实现中的错误。断言条件验证程序是否保持已定义状态。“已定义状态”基本上就是与程序的假设一致的状态。请注意,程序的“已定义状态”不一定是“理想状态”,甚至不一定是“通常状态”,甚至不一定是“有用状态”,但稍后将详细讨论这一重要问题。

要理解断言如何适应程序,请考虑 一个 C + + 程序,它将取消对指针的引用 例程在解引用之前测试指针是否为 NULL,或者 如果它断言指针不是 NULL,然后继续 无论如何都要取消引用?

我想大多数开发人员都会同时做这两件事,添加断言, 但也要检查指针是否为 NULL 值,以免崩溃 如果断言的条件失败,则执行 测试和检查似乎是最明智的决定

与其断言的条件不同,程序的错误处理(异常)不引用 对程序中的错误,但是对程序从其 这些通常是某些人的“错误”,比如用户 试图在不输入密码的情况下登录帐户 即使错误可能会阻止程序的成功完成 任务,没有程序失败。程序无法登录用户 由于外部错误而没有密码-用户的 如果情况不同,并且用户键入了 正确的密码和程序无法识别它; 然后虽然 结果还是一样,失败现在属于 这个项目。

错误处理(异常)的目的有两个,第一个是通信 向用户(或其他客户端)发送程序输入中的错误信息 第二个目标是恢复 应用程序在检测到错误后,处于定义良好的状态。注意 在这种情况下,程序本身不会出错 程序可能处于非理想状态,甚至可能处于某种状态 没有什么有用的,但没有编程错误。相反, 因为错误恢复状态是程序的 设计,这是一个程序可以处理。

附言: 你可能想看看类似的问题: 例外与断言