断言啥时候应该留在生产代码中? [关闭]
Posted
技术标签:
【中文标题】断言啥时候应该留在生产代码中? [关闭]【英文标题】:When should assertions stay in production code? [closed]断言什么时候应该留在生产代码中? [关闭] 【发布时间】:2008-08-20 11:01:19 【问题描述】:在 comp.lang.c++ 上有一个 discussion 正在讨论。关于断言(在 C++ 中默认仅存在于调试版本中)是否应该保留在生产代码中。
显然,每个项目都是独一无二的,所以我的问题是不是那么多是否应该保留断言,但在什么情况下这个值得推荐/不是一个好主意。
断言,我的意思是:
一种运行时检查,用于测试一个条件,当该条件为假时,会显示软件中的错误。 程序停止的一种机制(可能在非常少的清理工作之后)。我不一定在谈论 C 或 C++。
我自己的观点是,如果您是程序员,但不拥有数据(大多数商业桌面应用程序就是这种情况),您应该继续使用它们,因为失败的断言表明存在错误,而您不应该继续存在错误,有损坏用户数据的风险。这会迫使您在发布之前进行严格测试,并使错误更加明显,从而更容易发现和修复。
你的意见/经验是什么?
干杯,
卡尔
查看相关问题here
回复和更新
嘿,格雷厄姆,
断言是错误的,纯粹而简单,因此应该像对待一个断言一样处理。 由于应该在发布模式下处理错误,因此您实际上不需要断言。
这就是为什么我在谈论断言时更喜欢“错误”这个词。它使事情变得更加清晰。对我来说,“错误”这个词太模糊了。丢失的文件是错误,而不是错误,程序应该处理它。尝试取消引用空指针是一个错误,程序应该承认有些东西闻起来像坏奶酪。
因此,您应该使用断言测试指针,但使用正常的错误处理代码测试文件的存在。
有点离题,但在讨论中很重要。
提醒一下,如果您的断言在失败时会进入调试器,为什么不呢。但是文件无法存在的原因有很多,完全不受代码的控制:读/写权限、磁盘已满、USB 设备已拔出等。由于您无法控制它,我觉得断言是不是正确的处理方式。
卡尔
托马斯,
是的,我有 Code Complete,并且必须说我非常不同意该特定建议。
假设您的自定义内存分配器搞砸了,并将一块仍由其他对象使用的内存归零。我碰巧将这个对象定期取消引用的指针归零,其中一个不变量是这个指针永远不会为空,并且您有几个断言来确保它保持这种状态。如果指针突然为空怎么办。你只是 if() 围绕它,希望它有效?
请记住,我们在这里讨论的是产品代码,因此不会闯入调试器并检查本地状态。这是用户机器上的真正错误。
卡尔
【问题讨论】:
在 Software Engineering SE 上有一篇有趣的相关帖子(尽管讨论集中在 c++):Should there be assertions in release builds 【参考方案1】:断言是不会过时的 cmets。它们记录了哪些理论状态是预期的,哪些状态不应该出现。如果代码发生更改,因此状态允许更改,开发人员很快就会收到通知并需要更新断言。
【讨论】:
@jkschneider:单元测试用于测试程序代码范围内的事物。断言用于确保代码所基于的假设实际上是真实的,然后代码继续在这些假设下进行处理。从某种意义上说,它们是“文档”,如果事实证明并非如此,程序将中止,陈述假设,并表明假设不成立。当然,您也可以将断言作为代码中的文档来阅读。 这是最好的答案,因为它将断言与 cmets 相关联,这是思考它们的有用方式。它们比 cmets 更上一层楼,因为它们在开发过程中不断进行机器测试,但它们应该始终首先对人类读者有意义。就像 cmets 一样,它们不应该是逻辑或最终执行的一部分。就像 cmets 一样,您可以将它们保留或删除,具体取决于语言是编译还是解释、您的推出计划、混淆策略等。我见过一个评论实际上导致错误的案例,但那是一个奇怪的。 更具体地说,断言是信息性而不是功能性。断言本身对程序流程或结果没有影响。另一方面,一个例外会改变程序流程,从而改变结果。【参考方案2】:请允许我引用 Steve McConnell 的 Code Complete。关于断言的部分是 8.2。
通常,您不希望用户在生产代码中看到断言消息;断言主要用于开发和维护期间。断言通常在开发时编译到代码中,并在生产时从代码中编译出来。
但是,在同一部分的后面,给出了以下建议:
对于高度健壮的代码,断言然后处理错误。
我认为只要性能不是问题,就保留断言,而不是显示消息,而是将其写入日志文件。我认为该建议也在 Code Complete 中,但我现在找不到。
【讨论】:
我认为 Code Complete 的第二个引用意味着你应该有断言——它将在生产代码中编译出来——你应该也有“ if (!condition) AttemptGracefulRecovery(); ",即不要让违反程序不变量导致程序崩溃。【参考方案3】:在生产代码中保持打开状态,除非您测量到关闭断言后程序运行速度明显加快。
如果不值得衡量以证明它更有效,那么不值得为了性能赌博而牺牲清晰度。” - Steve McConnell 1993
http://c2.com/cgi/wiki?ShipWithAssertionsOn
【讨论】:
实际上,发生的最糟糕的事情是当代码由于非断言而崩溃时。如果代码以后肯定会以 100% 的概率崩溃,那么断言肯定存在。如果您取消引用一个指针,那么您必须在之前隐式断言它不为空。如果你除以一个数字,你断言它不是零。取出断言,所有崩溃位置都未记录。真正的问题不在于构建程序以让子系统崩溃并由看门狗重新启动。assert ref != null;
is different than if (ref == null) throw new IllegalArgumentException();
您不应该将第一个用于可能为假的先决条件。你需要使用assert
来处理不可能为假的事情。例如,int i = -1 * someNumber; i = i * i;
然后稍后提醒人们i
是积极的,assert i > 0;
"实际上,最糟糕的情况是代码由于非断言而崩溃。如果代码以后肯定会以 100% 的概率崩溃,那么断言肯定存在。 " - 这是一个错误的二分法。
@RobertGrant:对于许多程序来说,崩溃远不是可能发生的最糟糕的事情。对于一个应该检查建筑物或桥梁设计的健全性的程序,错误地报告设计是健全的可能是它所能做的最糟糕的事情。对于暴露给外部世界但对机密数据具有只读访问权限的程序,泄漏该数据可能比程序可以做的任何其他事情都更糟糕。没有有意义的原因说明的崩溃是“可能发生的最糟糕的事情”的概念忽略了许多更严重的危险。
@supercat 我不同意我引用的评论。【参考方案4】:
如果您甚至考虑在生产中保留断言,那么您可能认为它们是错误的。断言的全部意义在于您可以在生产中将其关闭,因为它们不是您解决方案的一部分。它们是一种开发工具,用于验证您的假设是否正确。但是当您投入生产时,您应该已经对自己的假设充满信心。
也就是说,有一种情况我会在生产中打开断言:如果我们在生产中遇到可重现的错误,而我们很难在测试环境中重现,那么重现该错误可能会有所帮助断言在生产中打开,看看它们是否提供有用的信息。
一个更有趣的问题是:在您的测试阶段,您什么时候关闭断言?
【讨论】:
我认为断言永远不应该包含在生产代码中。断言不是错误,它们是为开发人员设计的。断言应该只存在于测试代码中。应用程序崩溃是因为断言失败是不可接受的和草率的开发。开发人员需要加倍努力才能优雅地处理错误。 如果将一个空指针传递给 fn,您将不可避免地崩溃;没有选择明确地处理它。您要么有某种方式来优雅地处理条件(因为它可能来自外部世界的输入),要么您在带有断言的文档位置崩溃,而不是在沿途可能损坏事物的随机位置崩溃。但是,如何处理断言应该是每个模块的决定。也许您的看门狗重新启动了进程,或者您为该模块擦除了一块内存以将其重置为启动状态(软对象“重启”)。 我认为这是对断言使用的有限看法。我总是将断言记录到控制台和云存储中,并将它们留在生产环境中。保留断言确认即使在生产代码和生产使用中我的假设仍然正确。仅仅因为代码在使用断言的情况下在调试中成功运行了几次并不意味着用户不会找到通过相同代码路径传递不同值的方法。 断言语句的重点是您可以打开或关闭检查。如果您将它们留在生产中,为什么要使用 assert 语句? 断言通常会减慢系统速度。由于它们不是为生产而设计的,因此它们很慢且效率低下,这可能是执行某些测试所必需的。例如,微软曾经在 Excel 中添加了快速重新计算功能。当一个单元格发生变化时,此功能将重新计算限制为仅需要它的单元格。他们使用重新计算整个电子表格并比较结果的断言来测试这一点。这使得开发版本运行起来非常缓慢,但也排除了很多错误。当他们发布此功能时,事实证明它非常可靠。【参考方案5】:断言应该永远留在生产代码中。如果一个特定的断言看起来可能在生产代码中有用,那么它不应该是一个断言;它应该是运行时错误检查,即编码如下:if( condition != expected ) throw exception
。
“断言”一词的意思是“仅在开发时进行的检查,不会在现场执行。”
如果您开始认为断言可能会进入该领域,那么您将不可避免地开始产生其他危险的想法,例如想知道任何给定的断言是否真的值得做出。没有不值得做出的断言。你永远不应该问自己“我应该坚持这一点吗?”你应该只问自己“有什么我忘了断言吗?”
【讨论】:
【参考方案6】:除非分析显示断言导致性能问题,否则我说它们也应该保留在生产版本中。
但是,我认为这也需要您稍微优雅地处理断言失败。例如,它们应该产生一个通用类型的对话框,带有(自动)向开发人员报告问题的选项,而不仅仅是退出或使程序崩溃。此外,您应该注意不要将断言用于您实际上允许但可能不喜欢或认为不需要的条件。这些条件应该由代码的其他部分处理。
【讨论】:
在我看来,生产断言的主要目的是作为紧急后盾:允许程序继续运行很可能造成足够严重的伤害,以致于防止它比其他任何事情都更重要该程序可能正在执行。如果可能的话,有一个好的错误消息会很好,但这只是次要的。 assert 关键字的全部意义在于您可以在生产环境中关闭它们。如果您想将它们留在生产环境中,请不要使用断言语句。【参考方案7】:在我的 C++ 中,我定义了 REQUIRE(x),它类似于 assert(x),只是如果在发布版本中断言失败,它会引发异常。
由于失败的断言表明存在错误,因此即使在发布版本中也应认真对待。当我的代码的性能很重要时,我经常将 REQUIRE() 用于更高级别的代码,而将 assert() 用于必须快速运行的较低级别的代码。如果失败条件可能是由第三方编写的代码传入的数据或文件损坏引起的,我也使用 REQUIRE 而不是断言(最好我会专门设计代码以在文件损坏的情况下表现良好,但我们并不总是有时间这样做。)
他们说您不应该向最终用户显示这些断言消息,因为他们不会理解它们。所以?最终用户可能会向您发送一封电子邮件,其中包含屏幕截图或错误消息的一些文本,以帮助您进行调试。如果用户只是说“它崩溃了”,你就没有能力修复它。自动向自己发送断言失败消息会更好,但这只有在 (1) 软件在您控制/监控的服务器上运行或 (2) 用户可以访问互联网并且您可以获得他们发送的许可时才有效错误报告。
【讨论】:
他们还说你不应该向黑客展示这些断言信息,因为它们是入侵的宝贵线索。 如今,当大多数应用程序是客户端服务器时,您可以将断言失败添加到数据库中,因此您无需将它们显示给用户以获得反馈。 @DaveWalley 考虑替代方案是否更好。如果在最终构建中省略了断言,则代码以无效状态运行,结果可能无法预测 - 一个潜在的漏洞。如果没有报告该错误,它将如何修复?通常,普通用户的数量大大超过破解者,因此普通用户更有可能首先遇到该错误。除非该错误仅在破解者试图破解它时发生,在这种情况下,至少您检测到了问题并中止了操作。 如果生产代码中存在断言,则应将它们添加到日志中,该日志应转发给开发团队进行分析。这应该自动完成(这意味着软件应该在安装时对此达成某种协议)、半自动(需要一些报告工具)或手动完成(当用户报告支持时完成)。用户真的不应该看到它们,因为它充其量不会告诉他们任何有用的东西,或者最坏的情况是让他们感到困惑。无论哪种情况,都不是一个好的用户体验设计。【参考方案8】:如果您想保留它们,请使用错误处理替换它们。没有什么比一个程序消失更糟糕的了。我认为将某些错误视为严重错误并没有错,但它们应该被引导到您的程序的一部分,该部分可以通过收集数据、记录数据并通知用户您的应用程序有一些不需要的情况来处理它们正在退出。
【讨论】:
【参考方案9】:如果它们像任何其他错误一样被处理,我认为它没有问题。请记住,尽管 C 中的失败断言与其他语言一样,只会退出程序,而这对于生产系统通常是不够的。
有一些例外 - 例如,php 允许您为断言失败创建自定义处理程序,以便您可以显示自定义错误、进行详细日志记录等,而不仅仅是退出。
【讨论】:
【参考方案10】:我们的数据库服务器软件包含生产和调试断言。调试断言就是这样——它们在生产代码中被删除。只有在以下情况下才会发生生产断言:(a) 存在某些永远不会存在的条件,并且 (b) 不可能从该条件中可靠地恢复。生产断言表明软件中遇到了错误或发生了某种数据损坏。
由于这是一个数据库系统,并且我们正在存储潜在的企业关键数据,因此我们会尽一切努力避免数据损坏。如果存在可能导致我们存储不正确数据的情况,我们会立即断言、回滚所有事务并停止服务器。
话虽如此,我们也尽量避免在对性能至关重要的例程中进行生产断言。
【讨论】:
我将您的“生产断言”称为“异常”,并对其进行编码。 您可能是对的,但该产品最初是用 C 编写的。即使我们将其更改为 C++,我们在某些平台上使用的编译器也不能正确支持异常。大多数旧代码都没有用 C++ 重写,因此这些断言仍在使用。【参考方案11】:假设一段代码正在生产中,它遇到了一个通常会被触发的断言。断言发现了一个错误!除非它没有,因为断言已关闭。
那么现在会发生什么?程序要么 (1) 在远离问题根源的地方以无信息的方式崩溃,要么 (2) 愉快地运行到完成,可能会给出错误的结果。
这两种情况都不受欢迎。即使在生产中也让断言保持活动状态。
【讨论】:
【参考方案12】:我将断言视为内联单元测试。对于开发时的快速测试很有用,但最终这些断言应该被重构出来,以便在单元测试中进行外部测试。
【讨论】:
断言:陈述事实或信念。它们不是用于测试(仅),而是用于说明您认为正确的内容(即您的假设),因此如果您的假设由于某种原因是错误的,您的程序将不会继续。例如,这是对断言的有效使用:assert(pow(1,0) 【参考方案13】:我发现最好处理范围内的所有错误,并将断言用于我们断言为真的假设。
即,如果您的程序正在打开/读取/关闭文件,则无法打开该文件是在范围内——这是一种真正的可能性,换句话说,这将是疏忽大意的。所以,它应该有与之关联的错误检查代码。
但是,假设您的 fopen() 记录为始终返回有效的打开文件句柄。您打开文件,并将其传递给您的 readfile() 函数。
在这种情况下,可能根据其设计规范,readfile 函数几乎可以假设它将获得一个有效的文件 ptr。因此,在这样一个简单的程序中,为否定情况添加错误处理代码将是一种浪费。然而,在继续执行之前,它至少应该以某种方式记录假设——以某种方式确保——确实是这种情况。它实际上不应该假设它总是有效的,以防它被错误地调用,或者它被复制/粘贴到其他程序中,例如。
所以,readfile() assert(fptr != NULL); .. 在这种情况下是合适的,而全面的错误处理则不是(忽略实际读取文件需要一些错误处理系统的事实)。
是的,这些断言应该保留在生产代码中,除非绝对有必要禁用它们。即使这样,您也应该只在性能关键部分禁用它们。
【讨论】:
【参考方案14】:我很少将断言用于编译时类型检查以外的任何事情。我会使用 exception 而不是断言,因为大多数语言都是为了处理它们而构建的。
我举个例子
file = create-some-file();
_throwExceptionIf( file.exists() == false, "FILE DOES NOT EXIST");
反对
file = create-some-file();
ASSERT(file.exists());
应用程序将如何处理断言?我更喜欢旧的try catch
处理致命错误的方法。
【讨论】:
例外是针对您希望在工作应用程序中遇到的异常情况,例如此处的示例。断言适用于您希望永远不会遇到的情况。因此,如果您确实遇到它们,那么您的代码中肯定存在错误。在您的示例中,异常显然是正确的方法。但是断言对于捕捉错误仍然非常有用。【参考方案15】:大多数时候,当我在 java 中使用断言(assert 关键字)时,我会在之后自动添加一些生产代码。根据案例,它可以是日志消息、异常……或什么都没有。
据我所知,您的所有断言在开发版本中都至关重要,而不是在生产版本中。其中一些必须保留,另一些必须丢弃。
【讨论】:
【参考方案16】:ASSERTIONS 不是错误,不应作为错误处理。当一个断言被抛出时,这意味着你的代码或者调用你的代码的代码中存在错误。
有几点可以避免在生产代码中启用断言: 1. 您不希望最终用户看到类似“ASSERTION failed MyPrivateClass.cpp line 147。最终用户不是您的 QA 工程师。 2. ASSERTION 可能会影响性能
但是,留下断言有一个充分的理由: 断言可能会影响性能和时间,遗憾的是,这有时很重要(尤其是在嵌入式系统中)。
我倾向于投票支持在生产代码中保留断言,但要确保这些断言打印输出不会暴露给最终用户。
~伊兹克
【讨论】:
不,断言是针对假设的事实。如果我们的代码在假设它总是返回 25 时返回 27,那么问题也可能是我们对宇宙的物理假设中的一个错误:也许这两个位切换到 5 可能的值,这是计算历史上的第一次。该断言用于确认您的代码仍在其编写的假设下运行。如果物理超出了窗口,您的代码应该注意到,不要理会驱动器并在它前进时退出;)但是,这不是代码中的错误,您可以做什么样的错误处理? 请允许我完善我的意见: 1. 断言确实检查我们的假设。如果我们的假设是错误的,这意味着我们的代码中存在错误。 2. 我们的代码不应该断言我们代码的使用。即,如果用户的输入有问题,则函数不应在断言上失败。我们将返回错误并且用户应该处理(他可以断言成功) 3. 我更喜欢将断言留在生产中 - 但行为可能会被修改。我同意没有适当的错误处理。断言失败 == 错误.. 但系统可能会重新初始化,而不是停止并等待重新启动。 这并不一定意味着存在错误。例如,我参与的许多项目都在文档中附带了假设事实的列表。这些假设是为了保护开发人员的代码不被称为错误,例如,当业务人员可能告诉他们错误的事情时,或者当第三方没有关于特定变量的信息时。断言可用于验证程序应该/不应该运行,第三方系统是否正确,而不仅仅是 IT 是否正确。【参考方案17】:断言是错误的,纯粹而简单,因此应该像对待一个断言一样处理。
由于应该在发布模式下处理错误,因此您实际上不需要断言。
我看到断言的主要好处是有条件的中断 - 设置它们比钻取 VC 的窗口来设置需要 1 行代码的东西要容易得多。
【讨论】:
使用断言作为条件断点确实很烦人,原因有几个。最重要的是,这些断言让团队中的其他开发人员感到困惑——当断言触发时,他们如何知道这是一个错误,还是有人将他的条件断点留在了代码中? 如果代码中没有断言,就不会出现这种情况。如果您只使用它们来监控代码,您会看到它们(除非您将它们检入到源代码树中)。我曾在几乎对所有事情都有断言的地方工作过。由于帮助文档服务器不可用,必须在程序开始时单击大约 60 次,这很快就会让人厌烦。以上是关于断言啥时候应该留在生产代码中? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章
我啥时候应该使用 GRUB_TIMEOUT 选项? [关闭]