128

当通过契约编程时,函数或方法首先检查其先决条件是否得到满足,然后再开始履行其职责,对吧?进行这些检查的两种最突出的方法是 byassert和 by exception

  1. 断言仅在调试模式下失败。为了确保(单元)测试所有单独的合同先决条件以查看它们是否真的失败是至关重要的。
  2. 异常在调试和发布模式下失败。这样做的好处是测试的调试行为与发布行为相同,但会导致运行时性能损失。

你觉得哪一个更可取?

在此处查看相关问题

4

14 回答 14

199

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

于 2008-09-22T20:06:07.050 回答
39

在发布版本中禁用断言就像在说“我在发布版本中永远不会有任何问题”,但通常情况并非如此。所以断言不应该在发布版本中被禁用。但是您也不希望发布版本在发生错误时崩溃,对吗?

所以使用异常并很好地使用它们。使用良好、可靠的异常层次结构并确保您捕获并可以在调试器中对异常抛出进行挂钩以捕获它,并且在发布模式下您可以补偿错误而不是直接崩溃。这是更安全的方式。

于 2008-09-22T20:03:41.533 回答
24

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

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

例外情况是客户端和服务器都无法控制的情况。异常意味着出现了问题,并且没有任何办法可以避免它。例如,网络连接不在应用程序控制范围内,因此无法采取任何措施来避免网络错误。

我想补充一点,断言/异常的区别并不是考虑它的最佳方式。您真正想要考虑的是合同以及如何执行合同。在我上面的 URL 示例中,最好的办法是有一个封装 URL 的类,并且是 Null 或有效 URL。它是将字符串转换为 URL 来执行合同,如果无效则抛出异常。带有 URL 参数的方法比带有 String 参数和指定 URL 的断言的方法更清晰。

于 2008-09-22T20:13:47.330 回答
7

断言用于发现开发人员做错的事情(不仅是您自己 - 您团队中的另一位开发人员也是如此)。如果用户错误可能造成这种情况是合理的,那么它应该是一个例外。

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

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

于 2008-09-22T21:39:29.683 回答
5

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

在Bertrand Meyer 的Object Oriented Software Construction, 2nd Edition中,作者为在发布模式下检查先决条件敞开了大门。在这种情况下,当断言失败时会发生什么……引发断言冲突异常!在这种情况下,无法从这种情况中恢复:尽管可以做一些有用的事情,即自动生成错误报告,并且在某些情况下,重新启动应用程序。

这背后的动机是前置条件通常比不变量和后置条件更便宜,并且在某些情况下,发布版本中的正确性和“安全性”比速度更重要。即对于许多应用程序来说,速度不是问题,但健壮性(当程序的行为不正确时,即当合同被破坏时,程序以安全方式运行的能力)才是问题。

您是否应该始终启用前提条件检查?这取决于。由你决定。没有普遍的答案。如果您正在为银行开发软件,最好用一条警告消息中断执行,而不是转移 1,000,000 美元而不是 1,000 美元。但是,如果您正在编写游戏怎么办?也许你需要你能得到的所有速度,如果有人因为先决条件没有捕捉到的错误(因为它们没有被启用)而获得 1000 分而不是 10 分,那么运气不好。

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

总而言之,如果您启用它们,您可以拥有断言并仍然自动获取异常- 至少在 Eiffel 中是这样。我认为在 C++ 中做同样的事情你需要自己输入。

另请参阅:断言何时应保留在生产代码中?

于 2008-12-29T15:10:41.350 回答
2

关于在 comp.lang.c++.moderated 上的发布版本中启用/禁用断言有一个巨大的线索,如果你有几个星期的时间,你可以看到对此的看法是多么的多样化。:)

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

  1. 死于某种操作系统类型故障,导致调用中止。(无断言)
  2. 通过直接调用 abort 而死。(带断言)

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

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

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

于 2008-09-23T08:17:14.360 回答
2

我在这里概述了我对此事状态的看法:如何验证对象的内部状态?. 通常,主张您的主张并因他人违反而提出。要在发布版本中禁用断言,您可以执行以下操作:

  • 禁用断言以进行昂贵的检查(例如检查范围是否已排序)
  • 保持启用琐碎检查(例如检查空指针或布尔值)

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

于 2008-12-29T15:25:58.637 回答
1

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

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

例外是“嘿,用户,出了点问题”通知(显然,您可以编写代码来捕获它们,这样用户就不会被告知),但这些被设计为在 Joe 用户使用应用程序时在运行时发生。

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

不要忘记,许多前提条件都是用户提供的数据,因此您需要一种很好的方法来告知用户他的数据不好。为此,您通常需要将错误数据沿调用堆栈返回到他正在与之交互的位。断言将没有用 - 如果您的应用程序是 n 层,则更是如此。

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

于 2008-09-22T20:05:37.270 回答
0

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

于 2008-09-22T20:08:12.180 回答
0

前面的答案是正确的:对公共 API 函数使用异常。您可能希望改变此规则的唯一时间是检查的计算量很大。在这种情况下,您可以将其放入断言中。

如果您认为可能违反该前提条件,请将其保留为异常,或重构前提条件。

于 2008-09-22T20:51:18.957 回答
0

你应该同时使用两者。断言是为了方便您作为开发人员。异常会捕获您在运行时错过或未预料到的事情。

我越来越喜欢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;
}
于 2008-09-22T21:23:13.323 回答
0

我尝试在这里用我自己的观点综合其他几个答案。

在您想在生产中禁用它的情况下使用断言,而不是将它们留在其中。在生产中而不是在开发中禁用的唯一真正原因是加速程序。在大多数情况下,这种加速并不显着,但有时代码对时间要求很高,或者测试的计算量很大。如果代码是关键任务,那么尽管速度慢,异常可能是最好的。

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

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

于 2009-10-25T07:12:16.583 回答
0

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

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

要了解断言如何适合程序,请考虑 C++ 程序中即将取消引用指针的例程。现在例程应该在取消引用之前测试指针是否为 NULL,还是应该断言指针不为 NULL,然后继续取消引用它?

我想大多数开发人员都希望同时添加断言,但还要检查指针是否为 NULL 值,以免在断言条件失败时崩溃。从表面上看,同时进行测试和检查似乎是最明智的决定

与其断言的条件不同,程序的错误处理(异常)不是指程序中的错误,而是指程序从其环境中获得的输入。这些通常是某人的“错误”,例如用户尝试登录帐户而不输入密码。即使错误可能会阻止程序任务的成功完成,也不会有程序失败。由于外部错误,该程序无法在没有密码的情况下登录用户 - 用户方面的错误。如果情况不同,用户输入了正确的密码,程序无法识别;那么虽然结果还是一样,但失败现在属于程序。

错误处理(异常)的目的有两个。第一个是与用户(或其他一些客户端)沟通已检测到程序输入中的错误及其含义。第二个目标是在检测到错误后将应用程序恢复到明确定义的状态。请注意,在这种情况下,程序本身并没有错误。诚然,程序可能处于非理想状态,甚至是无用的状态,但不存在编程错误。相反,由于错误恢复状态是程序设计的一种预期状态,它是程序可以处理的状态。

PS:您可能想查看类似的问题:Exception Vs Assertion

于 2014-01-05T11:25:28.123 回答
-1

另请参阅此问题

在某些情况下,断言在构建发布时被禁用。您可能无法控制这一点(否则,您可以使用断言进行构建),因此这样做可能是个好主意。

“更正”输入值的问题是调用者不会得到他们期望的结果,这可能导致程序的完全不同部分出现问题甚至崩溃,使调试成为一场噩梦。

我通常在 if 语句中抛出一个异常来接管断言的角色,以防它们被禁用

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff
于 2008-09-22T20:18:22.967 回答