← 全部笔记

Note 01 工程

静默失败是最贵的 bug

6 MIN READ 阮家忆 工程 / 调试

三个项目里最贵的 bug,没有一个是崩溃。崩溃是慷慨的:它给你堆栈、行号、发生时间和一次可复现的机会。静默失败什么都不给,它只留下一个「按了没反应」的玩家,和三天之后才转达到你这里的一句抱怨。

我后来把它们分成三类。它们长得完全不一样,但有一个共同点——系统的输出依然「合法」。没有异常,没有红字,没有数字越界,只是某件事安静地没有发生。

判定被悄悄跳过

海战模拟里有一类很隐蔽的事故:飞机打舰船,弹道算出来是 NaN。NaN 与任何值比较都恒为 false,于是「是否命中」这个判定不会报错,它只是永远不成立。导弹穿过舰体,没有爆炸,没有播报,海面一片平静。

表面上这是数学问题,实际上是接口问题。那片代码里有两种实体:舰船只有一个位置对象,没有分开的三个坐标字段;飞机则在原型上挂了坐标访问器。某处直接读了一个不存在的字段,得到 undefined,再做一次减法就变成 NaN,然后一路安静地流进毁伤判定。

我最后的处理不是加日志,而是把读坐标收成一个写法:先看这个实体有没有坐标访问器,没有才回落到位置对象。把两条路改成一条路,这类 bug 就没有生存空间——它需要「有人走了另一条路」才能发生,而那条路已经不存在了。

写进去了,但没人读

生理模拟器有 32 种病理。加一种病要同时改四处:状态初始化、生理推导、病程表、界面注册。少改一处的后果不是报错,而是这种病「存在但不存在」——卡片能点,严重度会涨,病程也在跑,所有读数纹丝不动。

最典型的一次是抗生素。病理注册好了,病程也在演化,但免疫状态从来没有被任何一个生理量读过。药给了,感染没有变化,第一反应是「药效不够」,实际上是「这条通路根本没人消费」。代码本身完全合法:字段存在、赋值成功、没有任何人抱怨。

后来定了一条铁律:新病理键必须有真实生理消费者。测试里有一组静态检查,只读源码不跑模拟,把「病态键、病程表、生理消费者、界面注册」四向对齐。它抓不到精度问题,但能抓住空转——而空转恰恰是跑多久都发现不了的。

静默失败不是 bug 的一种。它是 bug 的藏身之处。 调试笔记 / 2026

算错了,但曲线很好看

单位换算的错误最像「正常」。剂量偏了一千倍,浓度曲线仍然是一条漂亮的指数衰减,受体占据仍然是一条标准的 S 形。差别只在数字上,而数字恰好是没人逐位核对的那部分。

这一类值得单独写一篇,因为它有一个非常坏的性质:错误不会让结果变丑,只会让结果变得可信。详见《一千倍的错误》。

把「安静」变成「吵闹」

三件很便宜的事,效果比任何调试器都好。

  1. 每一个「失败返回」都要给人一句话。不是给日志,是给屏幕上的人。不是「操作无效」,而是「目标超出射程」「挂载已用尽」「这架飞机还没准备好」。写第二条失败路径的时候,就必须同时写它的反馈——反馈不是收尾工作,它是这条路径的另一半。
  2. 每个关键概念只留一个入口。进座舱、取玩家位置、判断飞机是否可用、装配挂载,各只有一个函数,别处一律调用它。写第二套实现的时候不会立刻出错,它会在三个月后与第一套悄悄漂移,而漂移的那一天没有任何提示。
  3. 用探针,不用肉眼。海战那边是 20 个 Node 探针,改哪个模块就跑对应那一个,每个都带期望值;生理那边是十几个专项脚本,断言剂量-效应的单调性、分级的每一档、氧浓度梯度下的增益大小。探针的意义不是证明它对,而是让「它不对」有一个声音。
Checklist — 三个问题 任何一条新分支写完,问三句:这条失败谁会看见?这个值谁在消费?这个量纲谁在守?三个答案里只要有一个是「没人」,那它就是下一个静默失败。

模拟器尤其危险

普通程序出错,用户会看到一个坏掉的界面。模拟器出错,它会继续给你一个画面:海面照样有波浪,人体照样有心跳,数字照样在跳。它永远会给你一个结果——这是它最危险的地方。

所以工程上的大部分功夫不是让程序跑起来,而是让它在不该跑的时候,大声说出来。

Contact 交流

如果你也在做类似的东西,
欢迎写信。

806117943@qq.com ↗

物理、界面、数值方法,或者任何一个还没做完的模拟。