
先说一件刚发生的事。我们修掉了一处缺陷四种不同的「没有」被压成一个开关读数里分不出「设计如此」和「出错了」。修完接着做另一件事。那个新字段落地的第二天一位读者在评论里指出了同一处缺陷——并且写下了该怎么改。不是同一个问题复活。是同一类问题在我们以为自己已经学会的地方又长了一遍。这篇只讲这一件事记录里没有格子放「不知道」的时候「不知道」会读成「知道」——而且读成的是那个字段里最强的那句断言。## 一、四种「没有」一个开关先说背景。AI 写一条记录我们要留下它「写之前什么样、写之后什么样」的快照。将来要撤销得知道退回到哪里。问题出在「抓不到之后那份快照」这件事上。它至少有四种原因- 根本没装配抓取器——设计上的可选- 实体解析不到回退数据也是空的- 实体在但那行查不到——异常- 抓取过程抛错——错误前两种是设计选择的结果后两种是真出了事。而我们当时只有一个布尔字段。四种原因一个开关。于是有人问「这批快照为什么是空的」时答不上来分不出是设计如此还是坏了。更糟的是方向——那个开关默认是 false读起来就是「快照完好」。「没有」不是一个值是四种不同的值。压成一个布尔就是让最强的那种读法替所有情况作答。后来我们把它拆成四个成因分别落列、分别可读。拆的时候还发现最初拟的四个名字里有两个根本不属于这一类——一个在另一条路径上那是「写之前」那张表另一个是机制而不是成因。所以最后落地的四个和一开始想的四个并不一样。## 二、第二天在我们刚写完的地方修完这处接着做另一件事撤销之前记下「这一次我打算比对哪些成员」。承诺在这里是有意义的——撤销前是承诺撤销后是实况。两者对差才能说出「我承诺比对的某个成员其实比不了」。落地的做法是在组根行上放一个列装这次的差集这次没有差集就把列写空。看起来没问题。然后有人在评论里写下这段 一个按原因分列的字段放在成员行上会被最后一次撤销尝试覆写所以第二次尝试会擦掉第一次的「失约」。追加式能让它们分开。他是对的。而且比他说得还糟一点。那一列在没有差集时会被写成空而那个「空」同时表示两件事——「最近一次比对没有差集」和「这一行从来没有被撤销过」。所以- 第一次撤销发现「承诺了却比不到」——记下了- 第二次撤销一切正常——把第一次的记录写成了空- 事后读这一行空。读不出「曾经有过一次失约」也读不出「它被清空过」一次成功的重试抹掉了前一次的失败记录。要说清楚的是这个清空是有意的。理由写在当时的设计记录里——「陈旧的差集不该活过产生它的那次比对」。这个理由本身不算错。错的是那个「空」没有第二个意思可用。然后我们照他说的改了。改成每一次走到比对的尝试各占一行不再共用一个格子。两个读数在写的时候就分开这一行是「查出了差集」还是「查过、没有差集」。于是- 后一次尝试不再抹掉前一次——它写自己那一行- 空历史没有任何一次尝试走到过承诺与 「查过、干净」 不再是同一个读数- 那条旧规则——「陈旧的差集不该活过产生它的那次比对」——被撤回了。它错在用同一个格子表达两件事不是错在「不该留陈旧的东西」那句旧列没删作为遗留条目继续读标注「写于何时不可考」——删掉它恰好会抹掉这一整段争议的证据。## 三、同一套系统里有一处做对了上面两处都是「没有格子放不知道」。但同一套系统里有一处留了格子。撤销写到外部系统的时候我们只能把请求发出去终态在对方手里。对方返回 2xx只证明「请求到了」不证明「对方撤了」。所以那一条不写「已撤销」写「补偿已请求结果未知」——一个专门的读法。效果很直接没人会把它误读成完成。界面照实写「结果在目标系统」不必粉饰。所以这件事不是做不到。是有没有意识到要给「不知道」留位置。留了它就不塌没留它就归到最强的那种读法上。## 四、为什么难难在这个默认行为是静默的。每一个字段都在替你做决定而且都用最省事的那种读法- 空值默认读作「无此项」- 空集合默认读作「没有差异」- false默认读作「未标记」进而读作「正常」这三种读法在绝大多数情况下是对的。问题在剩下的那一小部分当「没有」和「不知道」共用同一个值你就得到了一个会撒谎的字段——而且它平时不吭声。更麻烦的是修一处不会让这一类消失——包括修它自己的时候。说一件就在那次修复里发生的事。改完之后「没有差集」有了自己的读法。但那个判据函数返回空其实有两种情形真的全比过了和压根没有可承诺的东西那时连「比对」都无从定义。第一种写「查过、干净」是对的第二种要是也写「查过、干净」就等于替一次从未发生的检查作证。我们差点就这么写了。发现之后改成什么都没承诺就不落行。所以这一类缺陷的入口在每一个「没有」上——你修一个它就在你写修复代码的那一行里等你。## 五、一个你可以自己做的检验不必问「我的记录全不全」先问一个更基础的问题把你系统里每一个表示「没有 / 未知 / 空」的值列出来逐个问它读成什么凡是同时承载两种含义的就是一处潜在的谎报。典型的三类| 写法 | 它同时意味着 ||---|---|| 空值 | 「没有这一项」/「查过结果为空」/「没查过」 || 空集合 | 「确实没有」/「没能读出来」 || false / 缺省值 | 「未标记」/「标记为否」 |判据很简单这个值是空的时候读者还能不能说清「为什么空」说不出来就是这个字段在替他猜。## 结语这类问题不好找因为它不报错。系统照常运行界面照常显示只是有那么一格在说它不是的话。而它最容易出现的地方恰恰是你刚刚修完的地方——因为那一刻你正忙着「已经知道该怎么做」了。交代一下我们自己的状态这个模式我们现在认识了也修了两处——第二处的形状是读者给的。但我们没有做过一次全盘体检那三类「没有」在我们的记录层里到底有多少处、分别读成什么我们没有清单。所以这篇不收在一个结论上。你拿上面那个检验去照自己的系统多半能找到几处我们这边只知道还没找到完。