通过合同编程时,函数或方法首先检查其前提条件是否已满足,然后才开始履行其职责,对吗?执行这些检查的两种最突出的方法是assert
和exception
。
您认为哪一个更受欢迎?
查看相关问题here
答案 0 :(得分:194)
经验法则是,当您尝试捕获自己的错误时应使用断言,并在尝试捕获其他人的错误时使用异常。换句话说,您应该使用异常来检查公共API函数的前提条件,以及何时获得系统外部的任何数据。您应该将断言用于系统内部的函数或数据。
答案 1 :(得分:39)
在发布版本中禁用断言就像是说“我将永远不会在发布版本中出现任何问题”,而事实并非如此。因此,不应在发布版本中禁用assert。但是,您不希望发生错误时发布版本崩溃,是吗?
因此,请使用异常并充分利用它们。使用一个好的,可靠的异常层次结构并确保捕获并且可以在调试器中抛出异常抛出钩子以捕获它,并且在释放模式下,您可以补偿错误而不是直接崩溃。这是更安全的方式。
答案 2 :(得分:22)
我遵循的原则是:如果通过编码可以现实地避免某种情况,那么使用断言。否则使用例外。
断言是为了确保遵守合同。合同必须公平,以便客户必须能够确保其符合要求。例如,您可以在合同中声明URL必须有效,因为关于什么是有效URL的规则是已知且一致的。
例外情况适用于客户端和服务器无法控制的情况。一个例外意味着某些事情出了问题,并且没有任何事情可以避免它。例如,网络连接在应用程序控制之外,因此无法避免网络错误。
我想补充一点,Assertion / Exception区别并不是考虑它的最佳方式。您真正想要考虑的是合同以及如何实施合同。在我上面的URL示例中,最好的办法是使用一个封装URL的类,它是Null或有效的URL。它是将字符串转换为强制执行合同的URL,如果无效则抛出异常。带有URL参数的方法比具有String参数的方法和指定URL的断言要清晰得多。
答案 3 :(得分:6)
断言是为了捕捉开发人员做错的事情(不仅仅是你自己 - 团队中的另一位开发人员)。如果用户错误可能会造成这种情况是合理的,那么它应该是一个例外。
同样考虑后果。断言通常会关闭应用程序。如果有任何现实的期望可以恢复条件,你应该使用例外。
另一方面,如果问题仅是由程序员错误引起的,那么请使用断言,因为您希望尽快了解它。可能会捕获并处理异常,您永远不会发现它。是的,您应该在发布代码中禁用断言,因为如果有可能的话,您希望应用程序恢复。即使你的程序状态被严重破坏,用户也可能能够保存他们的工作。
答案 4 :(得分:5)
“断言仅在调试模式下失败”并不完全正确。“
在Bertrand Meyer的面向对象软件构建,第2版中,作者打开了一扇门,用于检查发布模式中的前提条件。在这种情况下,当断言失败时会发生什么......提出了断言违例异常!在这种情况下,情况无法恢复:虽然可以做一些有用的事情,但它会自动生成错误报告,并在某些情况下重新启动应用程序。
这背后的动机是前提条件通常比不变量和后置条件更便宜,并且在某些情况下,发布版本中的正确性和“安全性”比速度更重要。即对于许多应用程序而言,速度不是问题,但健壮性(程序在其行为不正确时,即在合同被破坏时以安全的方式行事的能力)是。
您是否应始终启用前置条件检查?这取决于。由你决定。没有普遍的答案。如果您正在为银行制作软件,最好使用警报消息中断执行,而不是转移1,000,000美元而不是1,000美元。但是如果你正在编程游戏呢?也许你需要你可以获得的所有速度,并且如果有人得到1000分而不是10分,因为前提条件没有捕获的错误(因为它们没有启用),运气不好。
在这两种情况下,理想情况下,您应该在测试期间捕获该错误,并且应该在启用断言的情况下执行大部分测试。这里讨论的是对于那些由于测试不完整而未在早期检测到的情况下生产代码中的前提条件失败的极少数情况的最佳策略。
总结一下,如果你让它们保持启用,你可以拥有断言并仍然自动获得例外 - 至少在Eiffel中。我想在C ++中也一样,你需要自己输入它。
答案 5 :(得分:2)
关于在comp.lang.c ++。moderated上的发布版本中启用/禁用断言有一个巨大的thread,如果你有几个星期,你可以看到对此的看法有多么不同。 :)
与coppro相反,我相信如果您不确定在发布版本中是否可以禁用断言,那么它不应该是一个断言。断言是为了防止程序不变量被破坏。在这种情况下,就您的代码的客户而言,将会有两种可能的结果之一:
对用户没有任何区别,但是,断言可能会在代码中出现的代码中添加不必要的性能成本,而代码中的代码不会失败。
问题的答案实际上更多地取决于API的客户端是谁。如果您正在编写提供API的库,那么您需要某种形式的机制来通知您的客户他们错误地使用了API。除非你提供两个版本的库(一个带有断言,一个没有断言),否则断言是不太可能的选择。
然而,就个人而言,我不确定我是否会对这种情况采用例外情况。例外更适合可以进行适当形式的恢复的地方。例如,您可能正在尝试分配内存。当你捕获'std :: bad_alloc'异常时,可能会释放内存并重试。
答案 6 :(得分:2)
我在这里概述了我对此事的现状的看法:How do you validate an object's internal state?。一般来说,主张你的主张并抛弃他人的违规行为。要在发布版本中禁用断言,您可以执行以下操作:
当然,在发布版本中,失败的断言和未捕获的异常应该以与调试版本相同的方式处理(它可以只调用std :: abort)。在某处写入错误日志(可能写入文件),告诉客户发生了内部错误。客户将能够向您发送日志文件。
答案 7 :(得分:1)
您在询问设计时和运行时错误之间的区别。
断言是'嘿程序员,这是破坏'通知,他们在那里提醒你在发生错误时你不会注意到的错误。
例外是'嘿用户,有些事情出错'通知(显然你可以编写代码来捕获它们以便用户永远不会被告知)但这些是为Joe用户使用应用程序时的运行时设计的。
因此,如果您认为可以解决所有错误,请仅使用例外。如果你认为你不能......使用例外。您仍然可以使用调试断言来减少异常数量。
不要忘记许多前提条件都是用户提供的数据,因此您需要一种很好的方式来通知用户他的数据不好。为此,您通常需要将调用堆栈中的错误数据返回到与其交互的位。如果您的应用程序是n层,那么断言就没用了 - 加倍。
最后,我不使用 - 错误代码远远优于您认为会定期发生的错误。 :)
答案 8 :(得分:0)
我更喜欢第二个。虽然您的测试可能运行良好,但Murphy表示意外情况会出错。因此,不是在实际的错误方法调用中获得异常,而是最终找出更深层次的NullPointerException(或等效的)10个堆栈帧。
答案 9 :(得分:0)
在某些情况下,构建发布时会断言断言。你可以 无法控制这个(否则,你可以用断言构建 on),所以这样做可能是个好主意。
“纠正”输入值的问题在于调用者会 没有得到他们所期望的,这可能导致问题,甚至 在程序的完全不同的部分崩溃,进行调试 梦魇。
我通常在if语句中抛出异常来接管角色 如果它们被禁用,则断言
assert(value>0); if(value<=0) throw new ArgumentOutOfRangeException("value"); //do stuff
答案 10 :(得分:0)
之前的答案是正确的:对公共API函数使用异常。您可能希望弯曲此规则的唯一时间是检查计算成本高昂。在这种情况下,您可以将其置于断言中。
如果您认为可能存在违反该前提条件的情况,请将其作为例外处理,或者重构前提条件。
答案 11 :(得分:0)
你应该同时使用两者。断言是为了方便您作为开发人员。异常会捕获您在运行时遗漏或未预料到的内容。
我喜欢glib's error reporting functions而不是普通的断言。它们的行为类似于断言语句,但它们不是暂停程序,而是返回一个值并让程序继续运行。它的效果非常好,作为奖励,当一个函数没有返回“它应该是什么”时,你可以看到你的程序的其余部分会发生什么。如果它崩溃了,你知道你的错误检查在其他地方很麻烦。
在我的上一个项目中,我使用这些样式的函数来实现前置条件检查,如果其中一个失败,我会将堆栈跟踪打印到日志文件但继续运行。当其他人在运行我的调试版本时遇到问题时,节省了大量的调试时间。
#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;
}
答案 12 :(得分:0)
我尝试用自己的观点综合其他几个答案。
对于要在生产中禁用它的情况使用断言,错误地将它们留在其中。在生产中禁用而不是在开发中禁用的唯一真正原因是加速程序。在大多数情况下,这种加速速度并不显着,但有时代码是时间关键的,或者测试计算成本很高。如果代码是关键任务,那么尽管速度变慢,异常可能是最好的。
如果有任何真正的恢复机会,请使用例外,因为断言不是为了从中恢复的。例如,代码很少设计用于从编程错误中恢复,但它旨在从网络故障或锁定文件等因素中恢复。错误不应仅仅因为不受程序员控制而作为例外处理。相反,与编码错误相比,这些错误的可预测性使得它们更容易恢复。
重新论证调试断言更容易:来自正确命名的异常的堆栈跟踪与断言一样容易阅读。好的代码应该只捕获特定类型的异常,因此异常不应该因为被捕获而被忽视。但是,我认为Java有时会迫使您捕获所有异常。
答案 13 :(得分:0)
对我而言,经验法则是使用断言表达式来查找外部错误的内部错误和异常。您可以从G here的以下讨论中获益匪浅。
Assert表达式用于查找编程错误:程序逻辑本身中的错误或其相应实现中的错误。断言条件验证程序是否保持定义状态。 “定义状态”基本上与计划的假设一致。请注意,程序的“已定义状态”不一定是“理想状态”,甚至不是“通常状态”,甚至不是“有用状态”,而是稍后更重要的一点。
要了解断言如何适合程序,请考虑一个例程 一个即将取消引用指针的C ++程序。现在应该 例程测试在解除引用之前指针是否为NULL,或者 它应该断言指针不是NULL然后继续前进 无论如何取消引用它?
我想大多数开发人员都希望同时执行这两项操作,添加断言, 还要检查指针是否有NULL值,以免崩溃 如果断言条件失败。表面上,执行两个 测试和检查似乎是最明智的决定
与其声明的条件不同,程序的错误处理(例外)不是指 程序中的错误,但程序从其获得的输入 环境。这些通常是某些人的“错误”,例如用户 试图在不输入密码的情况下登录帐户。和 即使错误可能会阻止程序的成功完成 任务,没有程序失败。程序无法登录用户 没有密码由于外部错误 - 用户的错误 部分。如果情况不同,并且用户输入了 密码正确,程序无法识别;然后虽然 结果仍然是相同的,失败现在属于 该计划。
错误处理(例外)的目的有两个。首先是沟通 程序输入中的错误给用户(或其他一些客户端) 被发现并意味着什么。第二个目标是恢复 检测到错误后的应用程序,到一个定义良好的状态。注意 在这种情况下程序本身没有错误。当然, 程序可能处于非理想状态,甚至处于可以执行的状态 没什么用,但没有编程错误。反之, 因为错误恢复状态是程序预期的状态 设计,它是程序可以处理的一个。
PS:您可能想查看类似的问题:Exception Vs Assertion。