使用断言或异常按合同进行设计?[关闭]


123

通过合同进行编程时,功能或方法首先要检查其先决条件是否已满足,然后再开始执行其职责,对吗?两个最重要的方式做这些检查是通过assertexception

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

您认为哪一个更可取?

在这里查看相关问题


3
通过合同进行设计的全部要点是,您不需要(并且可以说不应)在运行时验证前提条件。您之前传递到方法与前提条件验证输入,那怎么你尊重你的合同结束。如果输入无效或违反了您的合同终止,则该程序通常会在其正常操作过程(您想要的)中始终失败。
void.pointer 2012年

这是一个很好的问题,但我认为您应该真正切换接受的答案(也如投票所示)!
DaveFar 2012年

我知道永远以后,但是这个问题实际上应该有c ++标记吗?我一直在寻找这个答案,用另一种语言(Delpih)使用,我无法想象任何具有异常和断言不遵循相同规则的语言。(仍在学习堆栈溢出指导原则。)
Eric G

响应给出的响应非常简洁:“换句话说,异常解决了应用程序的健壮性,而断言解决了应用程序的正确性。”
Shmuel Levine 2015年

Answers:


39

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

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


4
断言至少在检查正确性会导致效率低下或无法正确实施的情况下非常有用。
Casebash

89
断言的目的不是纠正错误,而是警告程序员。出于这个原因,将它们保持在发行版本中是没有用的:通过断言触发,您会获得什么?开发人员将无法进入并对其进行调试。断言是调试的辅助工具,它们不能代替异常(并且也不可以代替断言)。异常会提醒程序出现错误情况。断言提醒开发人员。
jalf

12
但是,当内部数据在修复后已损坏时,应使用断言-如果触发断言,您将无法对程序状态做出任何假设,因为这意味着/ wrong /。如果断言不成立,则不能假定任何数据有效。这就是为什么要发布版本的原因-不必告诉程序员问题出在哪里,而是要使程序可以关闭并且不会冒更大的风险。该程序应尽其所能,以在以后可以信任数据时促进恢复。
coppro

5
@jalf,尽管您无法在发行版本中加入调试器,但您可以利用日志记录,以便开发人员可以查看与断言有关的信息。Jim Shore 在本文档(martinfowler.com/ieeeSoftware/failFast.pdf)中指出:“请记住,在客户站点上发生的错误是通过测试过程造成的。您可能难以复制它。这些错误是最难找到的东西,而恰当地说明问题的断言可以为您节省很多时间。”
StriplingWarrior 2010年

5
我个人更喜欢通过合同方法进行设计的断言。异常是防御性的,正在函数内部进行参数检查。另外,dbc前提条件不会说“如果您使用的值超出工作范围,我将不会工作”,而是“我不能保证提供正确的答案,但我仍然可以做到”。断言为开发人员提供了反馈,即他们正在调用带有条件违规的函数,但是如果他们认为自己更了解,就不要阻止他们使用它。违反可能导致发生异常,但是我认为这是另一回事。
Matt_JD 2011年

194

经验法则是,当您尝试捕获自己的错误时应使用断言,而在尝试捕获其他人的错误时应使用异常。换句话说,无论何时获取系统外部的任何数据,都应使用异常检查公共API函数的前提条件。您应该对系统内部的功能或数据使用断言。


坐在不同模块/应用程序中并最终不同步的序列化/反序列化怎么办?我的意思是说,如果我尝试以错误的方式读取事物,因此我倾向于使用断言,那总是我的错误,但是另一方面,我拥有外部数据,这些数据最终可能会更改格式,而无需另行通知。
Slava 2014年

如果数据是外部数据,则应使用异常。在这种特殊情况下,您可能还应该捕获这些异常,并以某种合理的方式处理它们,而不仅仅是让程序死掉。另外,我的回答是经验法则,不是自然法则。:)因此,您必须分别考虑每种情况。
Dima 2014年

如果您的函数f(int * x)在其中包含x-> len行,则保证v被证明为null的f(v)会崩溃。此外,如果甚至在v上更早的时候被证明为空,但又证明f(v)被调用,那么您就有逻辑上的矛盾。这与具有a / b的情况相同,其中b最终被证明为0。理想情况下,此类代码应无法编译。除非假设是检查成本,否则关闭假设检查完全是愚蠢的,因为这会掩盖违反假设的位置。它至少必须被记录。无论如何,您应该具有崩溃时重启的设计。
罗布(Rob)2015年

22

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

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

客户端和服务器无法控制的情况除外。异常意味着出了点问题,没有什么可以避免的。例如,网络连接在应用程序控制之外,因此无法做任何事情来避免网络错误。

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


6

断言是用来捕获开发人员做错了的事情(不仅仅是您自己-团队中的另一位开发人员)。如果用户错误可能导致这种情况是合理的,则应该将其作为例外。

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

另一方面,如果问题是由于程序员错误引起的,则使用断言,因为您想尽快了解它。可能会捕获并处理异常,您将永远无法找到它。是的,您应该在发布代码中禁用断言,因为如果可能的可能性很小,您希望在那里恢复应用程序。即使您的程序状态被严重破坏,用户也可能能够保存其工作。


5

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

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

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

您是否应该始终启用前提条件检查?这取决于。由你决定。没有普遍的答案。如果您要为银行开发软件,则最好以一条警报消息中断执行,而不是转移$ 1,000,000而不是$ 1,000。但是,如果您正在编写游戏,该怎么办?也许您需要获得所有的速度,并且如果某个人由于未满足前提条件的错误(因为未启用)而获得1000分而不是10分,那么运气就很糟糕。

在两种情况下,理想情况下,您都应该在测试期间捕获到该错误,并且应该在启用断言的情况下进行测试的很大一部分。这里讨论的是对于那些在某些情况下前提条件在生产代码中失败的罕见情况的最佳策略,在这种情况下,由于未完成测试而导致早期无法检测到这种情况。

总而言之,您可以拥有断言,并且仍然可以自动获取异常(如果将它们保持启用状态)-至少在Eiffel中如此。我认为要在C ++中执行相同的操作,您需要自己输入。

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


1
您的观点绝对正确。所谓没有指定特定的语言-在C#中的情况下,标准的断言是System.Diagnostics.Debug.Assert,这只是在调试版本会失败,并会在发布版本编译时被删除。
yoyo

2

关于在comp.lang.c ++。moderated的发行版中启用/禁用断言有一个巨大的思路,如果您有几个星期的时间,就可以看到对此的看法有何不同。:)

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

  1. 死于某种OS类型的故障,导致调用中止。(没有断言)
  2. 通过直接中止死亡。(带断言)

用户之间没有区别,但是,断言有可能在代码中增加不必要的性能成本,而这些代码在绝大部分未失败的运行中都存在。

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

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


2

我在这里概述了对此问题的看法:如何验证对象的内部状态?。通常,声明您的主张并抛出他人侵犯。要在发行版本中禁用断言,您可以执行以下操作:

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

当然,在发行版本中,失败的断言和未捕获的异常应该以除调试版本(可以仅调用std :: abort)以外的其他方式处理。将错误日志写入某处(可能写入文件中),告诉客户发生内部错误。客户将能够向您发送日志文件。


1

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

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

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

因此,如果您认为可以消除所有错误,请仅使用异常。如果您认为自己无法...,请使用例外。您仍然可以使用debug断言来减少异常的数量。

不要忘记许多前提条件是用户提供的数据,因此您将需要一种很好的方式来告知用户他的数据不好。为此,您通常需要将错误数据从调用堆栈中返回到与之交互的位。断言将不会有用-双重,如果您的应用是n层。

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


0

我喜欢第二个。尽管您的测试可能运行良好,但墨菲说,出乎意料的事情会出问题。因此,您最终没有找到更深10个堆栈帧的NullPointerException(或等效的结果),而不是在实际的错误方法调用时获得异常。


0

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

如果您认为可能违反该先决条件,请将其保留为例外,或者将其重构。


0

您应该同时使用。断言是为您提供方便的开发人员。异常捕获您在运行时遗漏或未曾料到的事情。

我已经很喜欢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;
}

我认为我没有在OP中看到任何与C ++有关的问题。我认为您的答案中不应包含该内容。
ForceMagic

@ForceMagic:当我发布此答案时,该问题在2008年带​​有C ++标记,实际上C ++标记仅在5个小时前被删除。无论如何,该代码说明了与语言无关的概念。
2013年

0

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

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

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

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


0

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

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

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

我想大多数开发人员都想做这两个事情,添加断言,还要检查指针是否为NULL值,以防止断言条件失败时崩溃。从表面上看,执行测试和检查似乎是最明智的决定

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

错误处理(例外)的目的有两个。第一种是向用户(或其他客户端)传达已检测到程序输入错误及其含义的信息。第二个目标是在检测到错误后将应用程序还原到定义良好的状态。请注意,在这种情况下程序本身不会出错。当然,该程序可能处于非理想状态,甚至处于无用的状态,但是没有编程错误。相反,由于错误恢复状态是程序设计所预期的状态,因此它是程序可以处理的状态。

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


-1

另请参阅此问题

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

“校正”输入值的问题在于调用者将无法获得他们期望的结果,这可能导致问题甚至在程序的完全不同部分崩溃,从而使调试成为噩梦。

我通常在if语句中引发异常,以在断言被禁用的情况下取代断言的角色

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.