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