assert() 的现代用法指南
Assert(): A Modern How To
我一直对 assert() 语句感到困扰,它既是软件正确性的基石,又在某些方面表现不足。许多实现功能薄弱,缺乏深层上下文,且开发者常对何时使用、是否可用于生产环境感到困惑。文章提出了四个关键应用场景:正确性、安全性、开发和文档。通过断言,我们可以确保函数输入输出值的完整性,防止内存越界或溢出等安全隐患。在开发阶段,断言能作为细粒度的单元测试,验证逻辑假设;在文档层面,它们成为自解释的护栏。当断言被恰当地使用时,性能开销几乎可以忽略不计,却能显著提升代码的健壮性和可维护性。
当断言被恰当地使用时,它们几乎没有性能开销,我们谈论的只是读取本地变量并跳过分支的几个 CPU 周期,这对大多数应用来说甚至无法被准确测量。
HN 评论区
31- foo42
> 断言能在生产环境使用吗?
可以。
> 我应该对什么进行断言?
不变量(Invariants)。
> 我能自定义 assert 的行为吗?
它应该中止程序,并以 printf 风格记录堆栈以及你传入的上下文。如果它不中止程序,那只是把“程序处于错误内部状态”这个问题埋起来了。这永远不应该被接受。
- iTokio
有一种重叠的技术(覆盖了你使用 assert 的部分但非全部场景),那就是采用“解析但不验证”(parse-dont-validate)的方法,本质上是将“某个值已应用断言”这一事实编码到其类型中。
这种方法的易用性因语言而异,但基本思路是在某种构造函数中应用断言逻辑,然后阻止任何后续会破坏不变量的操作。保护这一点的简单方法是在可能的情况下将值设为不可变。
关心不变量是否成立的值的使用者,可以在类型中指定他们需要的是非空集合、foo-id 或任何类似的东西,而不是请求更宽泛的类型然后再进行断言。
- eps
我喜欢将断言与“可重启性”(restartability)结合起来。
如果你的程序进入了未知的失败状态,只需从已知状态重新启动它。
如果能将复杂系统划分为可以独立恢复而不会拖垮整个系统的子模块,那就更好了。
类似于 Erlang 的监督树(supervision tree),或者至少是 systemd 中 Restart=always 的服务。
如果你的程序主要是无状态的且“可重启”,它就具备了容错能力,你就可以自由地使用断言,并轻松避免未知或不良状态。
不变量可以得到强制执行,正确性得以保持。
但当一个断言被触发时,一个重要的问题依然存在:为什么不变量被违反了?
我们需要保留上下文,并决定是否处理这种情况。
在以断言为导向的代码中,这一点很容易被遗忘。