代码考古学:如何阅读没有人记得的代码

凌晨三点,报警响了。上线六小时的版本在打嗝:每隔几分钟,进程在退出流程里崩溃一次。回滚,天亮,复盘。

事故的起因是一个新人第一周提交的 PR,改动小得不好意思叫改动。他在 get_timeout_ms 里发现一段”明显多余”的代码:

int get_timeout_ms(void) {
  if (g_cfg == NULL) {       // 评审记录:"g_cfg 启动时就初始化了,不可能为 NULL"
    return DEFAULT_TIMEOUT;  // 新人删掉这两行,评审无人反对,合并,上线
  }
  return g_cfg->timeout_ms;
}

评审三个人看了一遍,都觉得他说得对:g_cfg 在 main 第三行就初始化了,全程没人置空,这个检查在任何一条正常路径上都不成立。删掉它是清理,不是冒险。

三班倒的崩溃说明:存在一条不正常的路径。关停流程里,热更新线程先释放 g_cfg,而退出排干的残留请求在这之后才会查询超时——窗口只有几十毫秒,白天流量小的时候撞不上,凌晨批量任务集中退出,每次都中。

回滚之后,有人对着这行被删掉的代码跑了 git blame(本文演示仓库实测,构造方法见附录 D):

57d40255 (zhangsan 2016-04-19 03:20:00 +0800 13)   if (g_cfg == NULL) {
57d40255 (zhangsan 2016-04-19 03:20:00 +0800 14)     return DEFAULT_TIMEOUT;
57d40255 (zhangsan 2016-04-19 03:20:00 +0800 15)   }

2016 年 4 月 19 日,凌晨三点二十。提交说明写着:fix: 关停竞态期间取默认超时(INC-2016-0417)。

这不是一行多余的代码,这是一座纪念碑。它纪念的是八年前另一个凌晨三点,另一个工程师对着另一个 core dump 熬过的那一夜。碑立在那里,只是没有碑文——评审记录、代码规范、静态检查那天晚上全都正常工作,唯一失效的环节是:没有人记得。

切斯特顿(G. K. Chesterton)在 1929 年写过一条栅栏定律:看到路上立着一道栅栏而不知道它的用途,我绝不会让你拆掉它。等你搞清楚它为什么立在那里,再来告诉我它没用了。这条定律在软件行业的引用率极高,遵守率极低——因为”搞清楚它为什么立在那里”这一步,没有任何工具自动替你完成。它是一门手艺。

这篇文章讲的就是这门手艺:代码考古学。文中的 git 输出全部在 Windows 11 + git 2.49.0 + PowerShell 实测;演示仓库由脚本按历史日期回填生成(GIT_AUTHOR_DATE),十三个提交模拟 2009 到 2023 年的沉积过程,构造脚本见附录 D,所有命令你可以原样复现(commit 哈希会因构造时间不同而不同)。演示代码同样过了编译验证:gateway.c、session.cpp、net_utils.cpp 在 Visual Studio 2026(cl 14.51,/W4 /utf-8 /std:c++17)下编译通过,其中 net_utils.cpp 另经 WSL Ubuntu 24.04 + gcc 13.3(-Wall -Wextra)验证——前两个文件依赖 Win32 API,Linux 侧天然不适用,双平台验证只对可移植层有意义。

一、前提拆除:代码的含义不在代码里

读遗留代码的第一课,是拆掉一个你从未意识到自己在坚持的前提:代码的含义在代码里。

这个前提在新代码上大体成立。你上周写的模块,意图还在你脑子里,约束还在评审记录里,变量名还有体温。读它,确实是读逻辑。

但遗留代码是另一回事。它的作者离职了;当年促成这个设计的约束(”那个客户还在用 IE6″”服务器只有 2G 内存””计费服务一压就垮”)无人记得;那次让所有人长记性的事故,连事故单都随工单系统的两次迁移一起蒸发了。你面对的不再是逻辑,而是一份作者永远缺席的历史文档。

一旦接受这个定位,整套方法论都要跟着换。因为代码和历史的关系是:

每一行代码,都是一连串意图、约束、事故和妥协的终态快照。写作是有损压缩,阅读是解压——而大部分”为什么”在压缩的那一刻就被丢弃了。

或者更狠一点:代码只记录判决,不记录庭审。

这个处境,调试系列的老读者其实见过。《每根栈都是清白的:程序”卡死”的鉴别诊断学》(暂未发布,上线后补链接)里有一种最刁钻的静止叫丢失唤醒:唤醒事件发生在过去,没有登记任何账本,你抓一百次 dump,一百次只能看到”等的姿势”。遗留代码就是源码世界的丢失唤醒:决策事件发生在过去,没有登记,你读多少遍代码,”为什么”那一维的信息量都是零。

所以阅读遗留系统不是”阅读理解”,是取证。你要像考古学家一样,用残存下来的冗余信息——地层、器物、铭文、外部档案——去重建那个没有被记录的信号。接下来的四节,就是考古学的四门手艺在代码库上的投影。

二、地层学:代码不是写的,是沉积的

考古学的第一原理是叠压律:在没有扰动的情况下,上层的遗物比下层的年轻。考古学家挖的从来不是土,是关系——1973 年 Edward Harris 发明的哈里斯矩阵(Harris matrix),把地层之间的叠压与打破关系画成一张有向图,单个地层本身并不含年代,年代存在于地层与地层的关系里。

这个洞察原样适用于代码:单文件无历史,历史在文件与提交的关系里。而一个真实代码库的地层,常常在同一个函数里握手。这是演示仓库里 session.cpp 的现状(实测文件):

DWORD WINAPI SessionThread(LPVOID lpParam) {   // 地层 A,2009:匈牙利命名 + Win32 风
  char szHost[256];                             // 地层 A:固定栈缓冲区
  DWORD dwLen;
  auto timeout = std::chrono::seconds(30);      // 地层 B,2016:C++11 改造波及到这里

  dwLen = GetPrivateProfileStringA("server", "host", "", szHost, 256, szIniPath);  // 地层 A:ANSI API
  if (dwLen == 0) {
    strcpy(szHost, "127.0.0.1");
  }
  std::string_view sv{szHost};                  // 地层 C,2023:后人的补丁
  printf("connecting %.*s\n", (int)sv.size(), sv.data());
  return 0;
}

三个年代,十四行。判读地层的第一件工具是碳测年:git blame 给每一行标出最后一次触碰它的提交。但 blame 有一个致命的扰动源,必须先说清楚。

扰动层:碳测年会重置

直接对演示仓库的 gateway.c 跑 blame(实测):

$ git blame gateway.c
f75b4237 (liwei    2013-02-11 ...)  // gateway.c - 网关转发与计费上报
...
0be7adbb (bot      2021-11-02 ...)  int get_timeout_ms(void) {
0be7adbb (bot      2021-11-02 ...)    if (g_cfg == NULL) {
0be7adbb (bot      2021-11-02 ...)      return DEFAULT_TIMEOUT;
...
0be7adbb (bot      2021-11-02 ...)    Sleep(50);
0be7adbb (bot      2021-11-02 ...)    return do_send(host, port, payload, retries);

几乎所有函数体行都显示 2021 年 11 月,作者 bot。这是 2021 年那次”全仓统一 clang-format 格式化”留下的扰动层:一次大规模重排,把几十年沉积的年代信息一夜抹平。考古学里这叫扰动——后世施工把地层打乱了。代码库里的扰动源不止格式化:批量改版权头、目录大迁移、行尾符转换,个个都是。

对策是把扰动层从测年里剔除。git 2.23 起支持 .git-blame-ignore-revs:把格式化提交的哈希写进这个文件(GitHub 的 blame 视图原生认它),再测(实测):

$ git blame --ignore-revs-file=.git-blame-ignore-revs gateway.c
f75b4237 (liwei    2013-02-11 ...)  int send_to_billing(...) {
f75b4237 (liwei    2013-02-11 ...)    int retries = 3;
c1795da7 (zhangsan 2014-11-21 ...)    if (is_internal(host)) {
40b05358 (liwei    2016-07-14 ...)    if (port == 8080) {
8ea81237 (zhangsan 2017-05-09 ...)    if (strcmp(host, "bill-backup") == 0) {
5475c450 (zhangsan 2018-11-02 ...)    Sleep(50);

年代归位:Sleep(50) 回到 2018 年,空指针检查回到 2016 年,每一段疤痕重新长回自己的地层。(建议顺手 git config blame.ignoreRevsFile .git-blame-ignore-revs,让它成为默认行为。)

洛阳铲:扰动层打不穿的钻探

更漂亮的性质在下面这个对比里。找 Sleep(50) 是什么时候进仓库的,用 pickaxe(实测):

$ git log -S "Sleep(50)" --oneline -- gateway.c
5475c45 fix: 下游计费服务限流,上报间隔 50ms(INC-2018-1102)

一击命中,而且结果里根本没有 2021 年那次格式化提交。原因在于 -S 的判定机制:它找的是”某个字符串出现次数发生变化”的提交。格式化重排了代码,但没有增删 Sleep(50) 这个字符串,所以在 pickaxe 眼里扰动层是透明的。碳测年会被扰动层重置,洛阳铲直接穿过去——断代工具要按污染机理选,这是地层学的第一条实用纪律。

地层学的最后一条洞察,也是最重要的:读懂了每一层的年代之后你会发现,bug 很少住在某一层内部,它住在层与层的接触面上。2009 年的地层假设”配置对象永生”,2016 年的补丁只在被烫过的地方打了补丁,2023 年的新代码又在这个假设上盖了新楼。这和卡死篇里”环不在任何一根栈里,在栈与栈的关系里”是同一个句式:遗址的病在关系里,不在单件器物里。

三、器物类型学:风格即年代

考古学家看一眼陶器的纹样和胎质,就能报出年代区间。程序员有同款能力,只是很少有人有意识地用:惯用法是年代的指纹。

器物(惯用法)年代层位
NULL、手写索引 for 循环C++11 之前
sprintf + 固定 char[] 缓冲区安全意识普及之前
A 后缀 API + ANSI 字符串Unicode 迁移完成之前
匈牙利命名法(dwLen、szHost)老 Windows 学派鼎盛期
裸 new/delete + try/catch 手工释放RAII 还没传到这个团队
宏当常量、#define MAX_RETRYconstexpr 之前,或 C 项目
错误码 + 返回值检查异常风格统一之前,或系统层传统

(完整版见附录 C。)类型学断代分三步:认器物,定年代,还原约束——第三步才是目的。一段”愚蠢”的手工内存管理,在没有智能指针可用的年代是理性选择;一个写死的 256 字节缓冲区,在主机名不超过 63 字节的年代绰绰有余。

这里必须立一块警示牌,否则整篇文章都会被读歪:用今天的工具箱审判当年的决策,是代码考古里的辉格史观。辉格史观是史学界的大忌——站在今天的结果上嘲笑古人的局限。在代码库里它的变体是:一边享受着 SSD、64G 内存和现代标准库,一边在评审里说”这写的什么垃圾”。器物类型学教你断代,不是教你傲慢。每一层丑代码都先问一句:当年换了我,手头这些工具,我写得更好吗?

四、铭文互证:单一证据不可信

地层给你年代,器物给你约束。但考古学家最想要的永远是文字材料——铭文。代码库里”自称记录了意图”的东西不少,问题是没有一种可以完全信任。干这行要先背一张证据等级表:

代码 > 测试 > commit message > 注释 > 文档 > 口头传说

逐个说透,每一条都是一门手艺。

代码排第一,因为它是唯一不可能说谎的证据——它是什么就是什么。但它只回答”是什么”,拒绝回答”为什么”。这就是全部问题的根源。

测试是活的证词:可执行,可证伪。但测试也会变成化石。一个测了八年的用例,证明的是”八年前的某人很在乎这个行为”,不证明”这个行为今天还重要”。读测试要先读它的年代,再决定它证词的效力。

commit message 是当代日记——写下它的人就在现场,时间上最接近庭审。好的 message 长这样(演示仓库实测,git show 5475c45):

commit 5475c450fa5821dd176c203fc9741cf87c21117d
Author: zhangsan <zhangsan@corp.example>
Date:   Fri Nov 2 02:47:00 2018 +0800

    fix: 下游计费服务限流,上报间隔 50ms(INC-2018-1102)

改了什么,为什么改,事故编号多少——三要素齐全,这是铭文里的上品。与之相对的是占行业大多数的”fix bug””update””改一下”:缺席的庭审,空白的判决书。

注释是后世注疏,不是原始铭文。这是最需要破除迷信的一条。注释描述的是”写下那一刻的代码”,代码变了它不变,腐烂速度比代码快得多。演示仓库里有一份铁证(实测,git blame config.h):

f75b4237 (liwei   2013-02-11 ...)  // 最多重试 3 次
e4480484 (chenjun 2019-08-15 ...)  #define MAX_RETRY 5

注释说 3,代码是 5。行级断代一目了然:这条注释比它描述的那行代码老了六岁半——2019 年的提交改了数字没改注释。注释不是不能读,是不能当原始证据读;它和代码冲突时,永远信代码,然后顺手把注释修掉(那是你给下一个考古学家的赠品)。

文档是理想国的地图,描述的是系统应该的样子。口头传说——老员工的记忆——排在最后,不是因为它没价值,恰恰相反,它常常是”为什么”的唯一来源;而是因为它会重构。记忆每次被提取都会被改写,老员工言之凿凿的”当年是为了性能”,可能只是他愿意记住的版本。口述史是合法证据,但必须交叉印证:用 commit、监控存档、外部系统的变更记录去对。印证不上的,标”存疑”,别标”采信”。

最后是一种可遇不可求的好运气:罗塞塔石碑。当你在仓库里发现新旧两套同构实现并存——老计费模块和正在灰度的新计费模块,C 版的解析器和 Rust 重写版——你手里就有了双重铭文。两套代码互为译文,哪边读通了都能解译另一边。考古学家靠一块三语石碑破译了埃及象形文字,你靠并行实现破译无人理解的遗留逻辑。遇到并行系统,先别嫌它乱,它是全仓库含金量最高的遗址。

五、发掘纪律:非破坏优先

考古和盗掘挖的是同一个坑,区别只有一条:记录。考古学家每揭一层都要拍照、编号、测绘,因为发掘本身就是破坏——地层一经翻动,原始关系永久消失。改遗留代码一模一样:你动的每一行,都在销毁它携带的历史信息。所以四门手艺里最后这门是纪律。

纪律一:先拍照,后动土。Michael Feathers 在《Working Effectively with Legacy Code》里给这件事起了正式名字:characterization test(特征化测试)。要点是不断言”正确”,只固定”现在”:

// 改造前先给现状拍照——钉住的是行为,不是道理
TEST(LegacyParser, Characterize_CurrentBehavior) {
    EXPECT_EQ(parse_price("1,234"), 1234);  // 现状如此,先钉住
    EXPECT_EQ(parse_price(""),      0);     // 哪怕它可能是 bug,也是今天的现实
}

照片的意义在于:动土之后任何行为漂移都会立刻报警。你拍的不是对错,是现状。

纪律二:一次只剥一层。地层要逐层揭,重构要逐层做。同时动三层的后果是出了问题无法归因——这和调试里”一次只改一个变量”是同一条戒律在两个战场的投影。

纪律三:写发掘日志。你花三天验证的假设、排除掉的死路、挖通的关系,写下来,进 commit message 或 wiki。开头那个凌晨三点的故事里,链条断裂的位置不是新人鲁莽,而是 2016 年那位工程师只留了半块碑——commit message 记了事故编号,但事故单今天已经打不开了。你要默认自己写下的每个字,都是下一个考古学家唯一的铭文来源。你今晚 save 的不只是代码,还有三年后某个凌晨三点的陌生人。

六、疤痕组织学:每一行丑代码都是一座纪念碑

四门手艺合在一起,就能干最实用的那件事:读变更模式。遗留代码的每一处”丑”,几乎都是一种化石。把演示仓库的 send_to_billing 按 blame 结果释读一遍(年代与作者均为实测):

int send_to_billing(const char* host, int port, const char* payload) {
  int retries = 3;                          // 2013 liwei:原始河床
  if (is_internal(host)) {                  // 2014 zhangsan:INC-2014-0331,内网重试放大雪崩
    retries = 1;
  }
  if (port == 8080) {                       // 2016 liwei:老客户端写死 8080
    port = 80;
  }
  if (strcmp(host, "bill-backup") == 0) {   // 2017 zhangsan:计费迁移期双写
    // TODO(zhangsan): 迁移期双写,下版本删除  <- 2017 年的"下版本"
    enqueue(payload);
  }
  Sleep(50);                                // 2018 zhangsan:INC-2018-1102,下游限流
  return do_send(host, port, payload, retries);
}

十行函数,五座纪念碑。这个函数不是”写”出来的,是一次事故接一次事故长出来的。推广开去,是一张化石对照表:

地表特征地下埋藏
越来越长的 if 条件链每个分支是一次事故或一个大客户
“这里不可能为 NULL”的防御检查它为 NULL 过,在生产上
注释掉的代码块舍不得埋的尸体,或待平反的冤案
feature flag / 配置开关政治妥协的停战协定(”先做成可关的”)
写着”兼容 XX”的代码与外部系统的和平条约,缔约方可能已消亡
六年没删的 TODO一个被新优先级永远推迟的承诺
两千行的函数沉积过快、常年无人清淤的主河床
几乎相同但略有差异的复制粘贴块平行地层:同一逻辑在不同分支各自演化

这张表的读法只有一句口诀:每一行丑代码都可能是某次线上事故的纪念碑,只是没刻碑文。考古学家的工作,就是补刻碑文——把年代、事故、约束查清楚,写进注释或 commit。补完你会发现一件反直觉的事:读懂之后的遗留代码往往没那么丑了。丑的不是代码,是失忆。

七、断代工程与无主文物

现实里的遗址很少像演示仓库这样铭文齐全。这一节处理三种残酷工况。

无主文物:被抹掉历史的仓库。公司从 SVN 迁 git 那年,有人图省事,把十年历史 squash 成一个提交——initial commit,五万行。blame 全灭,每一行都显示同一天。这种文物考古学里叫”无出处”,处理规程是:放弃行级断代,退回器物类型学(按惯用法把代码分成若干年代区块),加外部档案(老文档、邮件、发版记录),再加口述史。断代精度从”行”降到”区块”,但够用。

热点钻探:先挖哪里。遗址太大不可能全挖,考古学家靠勘测选点,程序员靠数据。Adam Tornhill 在《Your Code as a Crime Scene》——书名本身就是这门手艺最好的广告——给出的指标是热点 = 改动频率 × 复杂度。改动频率一行命令就有(实测):

$ git shortlog -sn -- gateway.c
     4  zhangsan
     2  liwei
     1  bot
$ git rev-list --count HEAD -- gateway.c
7

gateway.c 七年被改了七次、四个人经手,而 config.h 安静得多——改动最频繁又最复杂的文件,就是埋葬事故最密集的主河床,考古资源优先投给它。

行为钻探:什么时候变的。blame 回答”这行代码什么年代”,但很多时候你真正想问的是”行为在哪个层位改变”。MAX_RETRY 到底是哪天从 3 变成 5 的?对着十三层地层钻探,二分法(实测,完整过程):

$ git bisect start
$ git bisect bad HEAD
$ git bisect good f75b423
Bisecting: 4 revisions left to test after this (roughly 2 steps)
$ git bisect run sh -c "git grep -q 'MAX_RETRY 3' -- config.h"
running 'sh' '-c' 'git grep -q ...' 
Bisecting: 2 revisions left to test after this (roughly 1 step)
...
e4480484c3d4aa92b3fc693272efb3133e3c6fd9 is the first bad commit
commit e4480484c3d4aa92b3fc693272efb3133e3c6fd9
Author: chenjun <chenjun@corp.example>
Date:   Thu Aug 15 16:20:00 2019 +0800

    fix: 弱网环境重试 3 次不够,改为 5 次

两步钻探,定位到 2019 年 8 月 15 日那一层。(PowerShell 用户注意一个实测坑:给 sh -c 传带空格的探测命令时要用外双引号、内单引号,否则参数会被拆开,bisect 会沉默地给你错误答案——本文第一次就是这么踩的。)bisect 的本质是对地层做二分钻探,任何”什么时候行为变了”的问题——性能哪天开始退化的、这个头文件哪天开始被污染的——都是它的猎场。

最后是一对必须区分的概念:老,不等于死。五年没动的地层有两种可能:稳定,或者已经没人敢碰的僵尸。区分方法不是看年代,是看调用方——用引用分析查谁还在用它。还有调用方的老代码是成熟;没有调用方却没人敢删的老代码是废墟。前者供起来,后者清走。

八、主案例:那个 50 毫秒的 Sleep

把全部手艺装成一次完整办案。案情:网关服务的平均延迟比设计值高 50ms,不多不少,整整齐齐 50ms。有人怀疑网络,有人怀疑 GC。你不猜,你挖。

第一步,分诊。延迟整齐到毫秒级,多半是人为常量,不是系统噪声。目标锁定:找到那个常量。先别读代码——全仓上万行,读是最后手段。

第二步,钻探。洛阳铲直指嫌疑犯(实测):

$ git log -S "Sleep(50)" --oneline -- gateway.c
5475c45 fix: 下游计费服务限流,上报间隔 50ms(INC-2018-1102)

一条命中。看铭文(前文第四节引过全文):2018 年 11 月 2 日凌晨 2 点 47 分,zhangsan,事故编号 INC-2018-1102。

第三步,互证。拿着事故编号去查工单系统——查无此单。工单系统五年里迁移过两次,2018 年的数据只归档了标题,正文没了。这是真实考古的常态:铭文残缺,庭审记录损毁。别指望爽文结局,转向口述史:找到当年在团队里的老员工,他记得”计费服务好像被上游打爆过一次,后来他们加了限流”。再交叉印证:翻下游计费团队的 changelog,2018 年 11 月确有限流改版记录。证据链闭合:当年上游没有限流,我方每个请求都直怼计费,一次流量尖峰把对方打崩,事后约定我方主动限速 50ms——石碑立在了请求路径上。

第四步,结案。判定这座碑今天是否还承重:计费服务 2020 年已重写,自带限流和熔断,缔约的技术前提已消亡。但注意处置方式——不是直接删,那是盗墓。正确的结案是:把 50ms 改成配置项(默认保留原值),在代码里补刻碑文(注释写明 INC-2018-1102 与 2020 年下游改版两次事件),灰度调零观察两周,确认无回潮再彻底移除。整个发掘过程写进最终的 commit message——发掘日志闭环,碑文的碑文也立好了。

回看这次办案:钻探花了五分钟,互证花了两天。工具快的部分都不值钱,值钱的永远是证据链。

九、考古学的边界:什么时候别挖

写完八节方法论,必须写一节反方法论,否则这篇文章会成为另一种迷信。

按风险分诊,不按好奇心。要改的代码才值得挖两天;只是路过看到的丑代码,拍张照(加一行”此处历史待考”的注释)就走。考古资源永远稀缺,投给主河床,不投给路边每一块怪石。

反浪漫化。有些怪代码下面没有故事,就是当年周五晚上赶工写烂了。类型学能告诉你年代,不能赋予意义。挖了三层什么都没挖到时,要有勇气承认:这里不是遗址,是垃圾堆。垃圾堆的处理方式是清运,不是供起来。

栅栏定律的另一半。切斯特顿的原话是”搞清楚它为什么立在那里,再来拆”,重点在搞清楚,不在永远不拆。三段证据(地层、铭文、口述史)都查无收获时,做风险定价:影响面小就上 feature flag 灰度去试,让实验替考古回答;影响面大才值得继续深挖。把”没人知道为什么”当成永久豁免牌,那是对栅栏定律最大的曲解——它从护栏变成了挡箭牌。

重写是理性选项,但重写前仍要考古。当发掘成本持续高于重建成本,重写是对的。可重写不等于豁免考古:旧系统的疤痕清单必须完整继承到新系统,否则你会把每一道疤重新受一遍——行业里大部分”重写灾难”,灾难的不是重写本身,是把阑尾和疫苗疤一起切掉之后,旧病复发。

十、AI 时代:洛阳铲与考古学家

这篇文章写成于 AI 编程的喧嚣年代,回避这个话题是不诚实的。我的判断分三层,尽量克制。

AI 擅长地表测绘和类型学。把一个陌生仓库丢给它,它能秒级画出模块结构、生成摘要、识别每个区块的惯用法年代——这些恰好是考古里最耗时但最不需要”为什么”的工序。用它干这个,划算。

AI 读不了历史,而且会假装能读。“为什么”不在任何文本里,AI 和你一样无从得知。但它不会说不知道——它会基于代码的”合理性”幻觉出一个听起来很像那么回事的解释。这比没有解释更危险,因为它写得像真的。考古的铁律在 AI 时代加倍适用:任何”为什么”的答案,都必须有铭文或外部档案背书,不管它来自人还是模型。

真正的变化在沉积速率。AI 让代码生产提速一个数量级,意味着未来的地层将以十倍速度堆积——而且新地层里天然没有铭文,因为生成过程本就没有庭审。commit message、ADR、特征化测试这些”可考古性”实践,正在从工程美德变成生存需求。一句话收束:AI 是洛阳铲,不是考古学家。它让你钻探得更快,释读仍然是你。

十一、收束:做一具好尸体,留一块好碑

这个系列拆过好几个前提:调试篇拆了”作者知道意图”,海森堡篇拆了”观察不改变系统”,卡死篇拆了”程序卡住就是程序有病”。这篇拆的是最后一个、也最根本的一个:”代码的含义在代码里。”崩溃在空间上撒谎,卡死在时间上撒谎,代码在历史上沉默。

对付沉默的办法,前文已经讲了很多:地层学、类型学、铭文互证、发掘纪律。但手艺终究是下游,上游在我们自己手里。每个工程师每天同时干两份工作:写代码,以及制造未来的地层。第二份工作没有 KPI,但它决定三年后那个凌晨三点的陌生人要挖多久。

所以收束成三条铭文纪律:

  • commit message 记录 why 和事故编号,不记录 what。what 代码自己会说;why 你不说,就永远没人说了。
  • 重大决策写 ADR(架构决策记录):决策、备选、否决理由。它是有意识的墓葬铭文,是给未来考古学家备好的罗塞塔石碑。
  • 顺手补碑。读代码时挖通的每一段历史,写成注释或提交说明。你欠下一个考古学家的,正如上一个考古学家欠你的。

软件工程的大部分工作,其实是与不在场的人协作——与过去的作者,与未来的读者。代码考古学把这种跨时间的协作,从运气变成手艺。

你今天写的代码,就是某人明天要发掘的遗址。做一具好尸体,留一块好碑。

附录 A:发掘速查表

处境用手艺用工具
看不懂某段代码为何存在地层学 + 铭文互证blame(配 ignore-revs)/ log -S / 口述史
不敢动某段代码发掘纪律characterization test
要修 bug / 改行为疤痕组织学 + 行为钻探log -S / log -G / bisect
评估重写还是维护断代工程 + 热点分析shortlog / rev-list / 引用分析
历史被 squash 抹平无主文物规程类型学分区 + 外部档案 + 口述史

附录 B:git 断代命令对照

以下命令全部在 git 2.49.0 + PowerShell 实测(输出见正文):

git blame <file>                                   # 碳测年:行级年代
git config blame.ignoreRevsFile .git-blame-ignore-revs   # 永久排除扰动层
git blame --ignore-revs-file=.git-blame-ignore-revs <file>   # 临时排除
git log -S "字符串" --oneline -- <file>             # 洛阳铲:字符串何时引入/消失(按出现次数变化判定,免疫格式化)
git log -G "正则" --oneline -- <file>               # 按 diff 内容找变更
git log --follow --oneline -- <file>               # 追踪文件更名(不带 --follow 历史在更名处断裂)
git shortlog -sn -- <file>                         # 层的作者分布
git rev-list --count HEAD -- <file>                # 层位总数(改动频率)
git show <hash>                                    # 读铭文:某提交的完整证据
git bisect start / bad / good / run sh -c "..."    # 地层二分钻探:定位行为变更层位

两个实测坑:其一,PowerShell 里给 sh -c 传参用”外双引号 + 内单引号”,否则探测命令被拆词,bisect 给出静默错误答案;其二,-S 后面跟的是字面字符串,含正则元字符(如括号)无需转义,这正是它比 -G 适合精确钻探的原因。

附录 C:地层指纹表(C 与 C++)

器物年代层位当年约束
NULL、0 当空指针C++11 前没有 nullptr
匈牙利命名(dw/sz/lpsz 前缀)老 Windows 学派类型系统弱,靠命名补
A 后缀 API、ANSI 字符串Unicode 迁移前中文环境乱码史
sprintf、固定 char[N]安全意识前缓冲区溢出还没上头条
裸 new/delete 配手工释放RAII 前智能指针未普及
GetPrivateProfileStringA + ini 文件配置中心前单机部署年代
错误码返回值异常统一前跨编译器 ABI 顾虑
宏定义常量constexpr 前编译器优化能力弱
Sleep 当同步手段条件变量普及前并发原语匮乏
std::auto_ptrC++98–C++11 过渡期移动语义未诞生

用法:认器物 → 定年代 → 还原约束。记住辉格史观警示牌:指纹表用来理解,不用来嘲讽。

附录 D:演示仓库的构造方法

文中演示仓库由脚本生成:每次提交前设置 GIT_AUTHOR_DATE / GIT_COMMITTER_DATE(以及作者姓名邮箱),即可回填任意年代。核心模式如下(bash 版,PowerShell 完整脚本同思路):

GIT_AUTHOR_NAME=zhangsan  GIT_AUTHOR_DATE="2018-11-02 02:47:00 +0800" \
GIT_COMMITTER_NAME=zhangsan GIT_COMMITTER_DATE="2018-11-02 02:47:00 +0800" \
git commit -m "fix: 下游计费服务限流,上报间隔 50ms(INC-2018-1102)"

仓库共 13 个提交:2009 会话模块 → 2010 网络工具 → 2013 网关与配置 → 2014/2016/2017/2018 五道疤痕 → 2019 说谎的注释 → 2021 全仓格式化(扰动层)→ 2022 文件更名 → 2023 string_view 补丁。照此构造后,正文与附录 B 的每条命令可原样复现;commit 哈希会随构造时间变化,属正常现象。(PowerShell 用户注意:脚本文件若含中文,须保存为带 BOM 的 UTF-8,否则 PowerShell 5.1 按 ANSI 解析会产生乱码并破坏语法——本文构建脚本实测踩过。)

编译验证命令(对应正文的实测声明):

cl /nologo /W4 /utf-8 /c gateway.c
cl /nologo /W4 /utf-8 /std:c++17 /EHsc /c session.cpp
cl /nologo /W4 /utf-8 /std:c++17 /EHsc /c net_utils.cpp
gcc -x c -Wall -Wextra -c net_utils.cpp    # WSL 侧:仅可移植文件

两个开关不可省。其一,/std:c++17:MSVC 默认标准是 C++14,2023 年地层的 string_view 会直接报”不是 std 的成员”。其二,/utf-8:仓库文件是无 BOM 的 UTF-8(Linux 友好的存法),缺了它,MSVC 按本地代码页解码中文注释,实测中把 #include <windows.h> 所在行吞掉,Sleep 随之变成”未定义;假设外部返回 int”——一个编码问题伪装成了符号缺失。这两个坑本文都替你先踩过了。

相关文章