断言何时应保留在生产代码中?[关闭]


166

在comp.lang.c ++。moderated上正在进行一场讨论,讨论是否应将断言(应在C ++中默认情况下仅存在于调试版本中)保留在生产代码中。

显然,每个项目都是独特的,所以在这里我的问题是没有这么多是否断言应该保持,但在这情况下,这是值得推荐的/不是一个好主意。

断言是指:

  • 一种运行时检查,用于测试条件,该条件为false时,表明软件中存在错误。
  • 暂停程序的一种机制(可能是在清理工作最少之后)。

我不一定要谈论C或C ++。

我个人的意见是,如果您是程序员,但不拥有数据(大多数商业桌面应用程序就是这种情况),则应保留它们,因为断言失败会显示错误,并且您不应该去带有错误,可能会损坏用户的数据。这迫使您在发货之前进行严格的测试,并使错误更明显,从而更容易发现和修复。

您的看法/经验是什么?

干杯,

卡尔

在这里查看相关问题


回应和更新

嘿,格雷厄姆,

断言是错误,纯净而简单的断言,因此应像对待一个断言一样处理。由于应该在发布模式下处理错误,因此您实际上不需要断言。

这就是为什么我在谈论断言时更喜欢“ bug”一词的原因。它使事情变得更加清晰。对我来说,“错误”一词太含糊。丢失的文件是一个错误,而不是错误,程序应该对其进行处理。尝试取消引用空指针是一个错误,程序应该承认有些东西闻起来像不好的奶酪。

因此,您应该使用断言测试指针,但是使用正常的错误处理代码来测试文件的存在。


稍微偏离主题,但在讨论中很重要。

提醒您,如果断言失败时您的断言进入调试器,为什么不这样做。但是有很多原因导致文件不存在,这完全超出了代码的控制范围:读/写权限,磁盘已满,USB设备已拔出等。由于您无法控制它,因此我认为是断言不是正确的处理方式。

卡尔


托马斯

是的,我有完整的代码,必须说我完全不同意该特定建议。

假设您的自定义内存分配器已拧紧,并将仍由其他对象使用的内存块清零。我碰巧将这个对象定期取消引用的指针归零,其中一个不变之处是该指针永远不会为null,并且您有几个断言来确保它保持这种状态。如果指针突然为空,该怎么办。您只是围绕它if(),希望它能正常工作?

记住,我们在这里谈论产品代码,因此不会破坏调试器并检查本地状态。这是用户计算机上的真实错误。

卡尔


3
在软件工程SE上有一个有趣的相关文章(尽管讨论是针对c ++的):发布版本中应该有断言吗?
sonny 18'Apr

Answers:


84

断言是不会过时的注释。他们记录了预期的理论状态,以及不应发生的状态。如果代码被更改,因此声明允许更改,则开发人员将很快得到通知,并需要更新断言。


16
@jkschneider:单元测试用于测试程序代码范围内的内容。断言是为了确保在代码继续根据这些假设进行处理之前,代码所基于的假设实际上是真实的。从某种意义上说,它们是“文档”,如果事实并非如此,则程序将中止,陈述假设,并指出假设没有成立。当然,您也可以将断言读取为代码中的断言。

2
这是最好的答案,因为它使断言与注释相关联,这是一种思考它们的有用方法。它们是注释的基础,因为它们在开发过程中一直在经过机器测试,但是对于人类读者而言,它们首先应该始终有意义。就像注释一样,它们不应成为逻辑或最终执行的一部分。就像注释一样,您可以根据语言是编译还是解释,发布计划,混淆策略等将其保留或删除。我已经看到一种情况,注释实际上导致了错误,但是那是一个怪异的
DaveWalley 2014年

1
更具体地说,断言是信息性的,而不是功能性的。断言本身对程序流程或结果没有影响。另一方面,异常会更改程序流程并因此改变结果。
yoyo

59

请允许我引用史蒂夫·麦康奈尔的《代码完成》。关于断言的部分是8.2。

通常,您不希望用户在生产代码中看到断言消息。断言主要用于开发和维护期间。断言通常在开发时编译到代码中,并从代码中编译出来用于生产。

但是,在同一部分的后面,提供了以下建议:

对于高度健壮的代码,请断言然后再处理错误。

我认为,只要性能不是问题,就保留断言,而不是显示消息,而是将其写入日志文件。我认为建议也包含在“代码完成”中,但是我现在找不到。


7
我想从代码完成表示第二个引号是你应有的断言-这将在生产代码被编译出来-你应该有“如果{AttemptGracefulRecovery();}(条件!)”,即不要不要让违反程序不变性的程序崩溃。
yoyo

34

使断言在生产代码中保持打开状态,除非您已断言它们已使程序运行得明显更快。

如果不值得通过测量来证明它更有效,那么就不应该为了提高性能而牺牲清晰度。”-史蒂夫·麦康奈尔1993

http://c2.com/cgi/wiki?ShipWithAssertionsOn


11
实际上,最糟糕的事情是当代码由于不是断言而崩溃时。如果代码以后肯定会以100%的概率崩溃,那么断言必须绝对存在。如果取消引用指针,则必须隐式断言它之前不是null。如果用数字除,则断言它不是零。取出断言,所有崩溃位置均未记录。真正的问题不是构造程序让子系统崩溃并由看门狗重新启动。
罗布(Rob)2014年

5
assert ref != null;if (ref == null) throw new IllegalArgumentException();您不应该为可能为假的先决条件使用first 有所不同。您需要使用不能为假的assert东西。例如,然后稍后提醒人们肯定的是,int i = -1 * someNumber; i = i * i;iassert i > 0;
Man

1
“实际上,最糟糕的事情是当代码由于不是断言的东西而崩溃时。如果代码以后肯定会以100%的概率崩溃,那么断言必须绝对存在。” -这是错误的二分法。
罗布·格兰特

2
@RobertGrant:对于许多程序来说,崩溃绝不是最糟糕的事情。对于应该检查建筑物或桥梁设计的健全性的程序,错误地报告设计合理可能只是它可能做的最坏的事情。对于暴露于外界但具有对机密数据的只读访问权限的程序,泄漏该数据可能比该程序可以做的任何其他事情都要糟糕。没有有意义的原因指示的崩溃是“可能发生的最糟糕的事情”的观念忽略了许多更糟的危险。
supercat

@supercat我不同意我所引用的评论。
罗伯·格兰特

21

如果您甚至想在生产中使用断言,那么您可能会认为它们是错误的。断言的全部目的是您可以在生产中将其关闭,因为它们不是解决方案的一部分。它们是一种开发工具,用于验证您的假设正确。但是,当您投入生产时,您应该已经对自己的假设充满了信心。

就是说,在一种情况下,我会在生产中打开断言:如果我们在生产中遇到可重现的错误,而我们在测试环境中很难重现,则在打开断言时重现该错误可能会有所帮助。在生产中,看看它们是否提供有用的信息。

一个更有趣的问题是:在测试阶段,什么时候关闭断言?


4
我认为断言永远不应包含在生产代码中。断言不是错误,它们是为开发人员设计的。断言应仅存在于测试代码中。导致应用崩溃的原因是,断言无法接受且开发工作马虎。开发人员需要付出更多努力才能优雅地处理错误。
iksnae 2014年

9
如果不可避免的话,将空指针传递给fn会使您崩溃。没有明确地处理它的选择。您或者有某种方式来妥善处理该条件(因为它可能来自外界的输入),或者您在断言处位于DOCUMENTED位置,而不是在途中损坏了位置的随机位置。断言的处理方式应该由每个模块决定。也许您的看门狗重新启动了进程,或者您擦除了该模块的大块内存以将其重置为启动状态(软对象“重新启动”)。
罗布2014年

1
我认为这是对使用assert的有限观点。我总是将断言记录到控制台和云存储中,然后将其继续投入生产。Leaving on断言表明,即使在生产代码和生产用法中,我的假设仍然正确。仅仅因为在断言打开的情况下代码在调试中成功运行了几次并不意味着用户不会找到通过同一代码路径传递不同值的方法。
SafeFastExpressive

assert语句的全部目的是您可以打开或关闭检查。如果将它们保留在生产中,为什么要使用assert语句?
MiguelMunoz

1
断言通常会降低系统速度。由于它们不是为生产而设计的,因此可以放慢速度和效率,这对于执行某些测试可能是必需的。例如,Microsoft曾经向Excel添加了快速重新计算功能。当一个单元更改时,此功能将重新计算限制为仅需要它的单元。他们用一个断言测试了这一点,该断言重新计算了整个电子表格并比较了结果。这使开发版本的运行速度非常慢,但同时也消除了许多错误。当他们发布此功能时,事实证明它非常可靠。
MiguelMunoz

16

断言永远不应停留在生产代码中。如果一个特定的断言似乎在生产代码中可能有用,那么它就不应该是一个断言。它应该是运行时错误检查,即类似以下代码:if( condition != expected ) throw exception

“断言”一词的意思是“仅开发时间检查,不会在现场执行”。

如果您开始认为断言可能会成为现实,那么您不可避免地也会开始提出其他危险的想法,例如想知道任何给定的断言是否真的值得做出。没有没有价值的主张。您永远不要问自己“我应该坚持还是不坚持?” 您只应该问自己:“我有什么要断言的吗?”


6

除非分析表明断言导致性能问题,否则我说它们也应保留在生产版本中。

但是,我认为这还要求您一定程度地处理断言失败。例如,它们应该导致一种常规类型的对话框,并带有(自动)向开发人员报告问题的选项,而不仅仅是退出程序或使程序崩溃。另外,您应注意不要对您确实允许但可能不喜欢或认为不需要的条件使用断言。这些条件应由代码的其他部分处理。


正如我所看到的,生产声明的主要目的是作为紧急支持:允许程序继续运行很可能造成非常严重的危害,以至于防止它比程序可能要做的其他任何事情都更为重要。如果可能的话,有一个好的错误消息会很好,但这仅是次要的。
supercat

5

在我的C ++中,我定义REQUIRE(x)类似于assert(x),不同之处在于,如果在发布版本中断言失败,它将抛出异常。

由于断言失败表明存在错误,因此即使在发布版本中也应认真对待。当我的代码的性能很重要时,我经常将REQUIRE()用于高级代码,将assert()用于必须快速运行的低级代码。如果失败情况可能是由第三方编写的代码传入的数据或文件损坏引起的,我也使用REQUIRE来代替断言(最佳情况下,我将代码专门设计为在文件损坏的情况下表现良好),但是并非总是有时间这样做。)

他们说您不应该向最终用户显示那些断言的消息,因为他们不会理解它们。所以?最终用户可能会向您发送一封电子邮件,其中包含屏幕截图或错误消息的某些文本,可帮助您进行调试。如果用户只是说“它崩溃了”,则您修复它的能力将降低。最好通过Internet自动向您发送断言失败消息,但这仅在用户具有Internet访问权限并且您可以征得他们的许可的情况下才有效。


他们也说,你不应该表现出这些断言消息,黑客,因为他们是有价值的线索打破。
DaveWalley

4

如果要保留它们,请用错误处理替换它们。没有什么比消失的程序更糟糕了。我认为将某些错误视为严重的错误没有错,但是应该将它们定向到程序的一部分,该部分可以通过收集数据,对其进行记录并通知用户您的应用程序存在一些不良情况来处理它们,并且正在退出。


2

如果像对待其他任何错误一样处理它们,则不会出现任何问题。请记住,尽管与其他语言一样,用C失败的断言只会退出程序,并且这通常不足以用于生产系统。

有一些例外情况-例如,PHP允许您创建断言失败的自定义处理程序,以便您可以显示自定义错误,进行详细的日志记录等,而不仅仅是退出。


2

我们的数据库服务器软件包含生产和调试断言。调试断言仅是-在生产代码中将其删除。制作断言只有(一)一些条件存在发生应该永远存在和(b)是不能够可靠地从这种状态中恢复。生产断言表明已遇到软件错误或某种数据损坏。

由于这是一个数据库系统,并且我们正在存储潜在的企业关键数据,因此我们将尽一切可能避免损坏数据。如果存在可能导致我们存储错误数据的条件,我们将立即声明,回滚所有事务并停止服务器。

话虽如此,我们还试图避免在性能关键型例程中进行生产断言。


5
我将您的“生产断言”称为“例外”,并进行编码。
DaveWalley 2014年

1
您可能是对的,但该产品最初是用C编写的。即使将其更改为C ++,我们在某些平台上使用的编译器也无法正确支持异常。大多数旧代码都没有用C ++重写,因此仍使用这些断言。
Graeme Perrow 2014年

1

我认为断言是在线单元测试。对于在开发过程中进行快速测试很有用,但最终应将这些断言重构为在单元测试中进行外部测试。


断言:陈述事实或信念。它们不是用于测试(仅用于测试),而是用于说明您认为正确的内容(即您的假设),因此如果您的假设由于某种原因是错误的,则程序将不会继续。例如,这是断言的有效用法:assert(pow(1,0)<1)。它实际上不是错误检查的合适场所,因为如果这不是真的,那么现代数学几乎都错了,而且……那么,您将如何开始处理呢?处理不正确的假设超出程序范围;你相信它。但是您还是要验证。

1

我发现最好处理范围内的所有错误,并将断言用于我们断言ARE true的假设。

即,如果您的程序正在打开/读取/关闭文件,则无法打开文件属于范围内-这是一种真实的可能性,换句话说,这是疏忽大意的。因此,应该具有与其相关的错误检查代码。

但是,假设您的fopen()被记录为始终返回有效的打开文件句柄。您打开文件,并将其传递给您的readfile()函数。

在这种情况下,并且可能根据其设计规范,该readfile函数几乎可以假设它将获得有效的文件ptr。因此,在如此简单的程序中为否定情况添加错误处理代码将是浪费的。但是,在继续执行之前,它至少应以某种方式(确保以某种方式)记录这种假设,以确保确实如此。例如,如果不正确地调用它,或者将其复制/粘贴到其他程序中,则不应假定它始终有效。

因此,readfile(){assert(fptr!= NULL); ..}在这种情况下是合适的,而全面的错误处理则不适用(忽略这样的事实:实际读取文件仍然需要某种错误处理系统)。

是的,除非断言绝对必要,否则这些断言应保留在生产代码中。即使这样,您也可能仅应在性能至关重要的部分中禁用它们。


1

假设有一段代码正在生产中,并且它命中了一个通常会触发的断言。该断言发现了一个错误!除非它没有,否则因为断言已关闭。

那么现在会发生什么呢?程序要么(1)以一种非信息性的方式崩溃,然后又从问题的根源移开,或者(2)顺利运行至完成,可能会给出错误的结果。

两种情况均不诱人。即使在生产中也要使断言保持活动状态。


0

除了编译时间类型检查之外,我很少使用断言。我会使用异常而不是断言,因为大多数语言都是为处理它们而构建的。

我举一个例子

file = create-some-file();
_throwExceptionIf( file.exists() == false, "FILE DOES NOT EXIST");

反对

file = create-some-file();
ASSERT(file.exists());

应用程序将如何处理断言?我喜欢try catch处理致命错误的旧方法。


2
异常是指在正常工作的应用程序中可能遇到的异常情况,例如此处的示例。断言适用于您料想从未遇到过的情况。因此,如果确实遇到它们,则您的代码中肯定有一个错误。在您的示例中,异常显然是正确的方法。但是断言对于捕获错误仍然非常有用。
MiguelMunoz

0

大多数时候,当我在Java中使用断言(assert关键字)时,我会在之后自动添加一些生产代码。根据情况,它可以是日志消息,异常……或什么都没有。

根据我的说法,您的所有主张对开发版本至关重要,而不对生产发布至关重要。其中一些必须保留,其他必须丢弃。


0

声明不是错误,不应将其视为错误。引发断言时,这意味着您的代码或调用您的代码的代码中存在错误。

有几点要避免在生产代码中启用断言:1.您不希望最终用户看到诸如“ ASSERTION failed MyPrivateClass.cpp line 147.这样的消息。最终用户不是您的QA工程师。2. ASSERTION可能影响表现

但是,有一个强有力的理由留下断言:ASSERTION可能会影响性能和时序,但遗憾的是,这有时很重要(尤其是在嵌入式系统中)。

我倾向于投票赞成将断言保留在生产代码中,但要确保这些断言的打印输出不暴露给最终用户。

〜伊兹克


不,断言是针对假定的事实。如果假设我们的代码总是返回25,则返回27,则该问题也可能是我们对宇宙的物理假设中的一个错误:也许这两位在计算历史上首次转换为5可能的值。声明可以确认您的代码仍在为其编写的假设下运行。如果物理学超出了人们的视野,您的代码应该注意,让驱动器保持静止并在前进时退出;)但是,是的,这不是代码中的错误,您可以执行哪种错误处理?

请允许我完善我的意见:1.断言可以检查我们的假设。如果我们的假设是错误的,则意味着我们的代码中存在错误。2.我们的代码不应断言我们代码的使用。即,如果来自用户的输入有问题,则函数不应在断言时失败。我们将返回错误,用户应该处理(他可以断言成功)。3.我更喜欢将断言留在生产中-但是行为可能会被修改。我同意没有适当的错误处理。断言失败==错误..但系统可能会重新初始化自身,而不是停止并等待重新启动。
Yitshak Yarom 2013年

1
它不一定表示存在错误。例如,我参与的许多项目都在文档中附带了一些假定的事实。这些假设可以防止开发人员的代码被称为错误,例如当商人可能告诉他们错误的消息时,或者当第三方无法提供有关特定变量的信息时。断言可用于验证程序是否应该/不应运行,第三方系统是否正确,而不仅仅是IT是否正确。

-8

断言是错误,纯净而简单的断言,因此应像对待一个断言一样处理。

由于应该在发布模式下处理错误,因此您实际上不需要断言。

我看到的断言的主要好处是有条件的中断-与通过VC窗口进行钻取来设置需要1行代码的代码相比,它们的设置要容易得多。


2
使用断言作为条件断点确实很烦人,原因有几个。最重要的是,这些断言使团队中的其他开发人员感到困惑-当断言被断言时是怎么回事,还是有人将他的条件断点留在了代码中呢?
lego 2012年

如果断言不在代码的首位,那就没有了。而且,如果仅使用它们来监视代码,则您将看到它们(除非您将它们检入到源代码树中)。我在几乎可以断言的地方工作。由于该帮助文档服务器不可用,在程序启动时必须单击约60次,这会很快变得很烦。
graham.reeds 2012年
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.