CLASS 13 · 2026-03-30 · 11-13 递归与解析
调试
调试的第一原则是尽量不去调试:用类型系统、检查和不变性让 bug 在写下的那一刻就无处藏身。当 bug 仍然出现时,用科学方法系统地复现、假设、实验和修复,而不是靠运气。
常见 bug 类型
在动手之前,先认识几类最常出没的 bug,有助于你快速识别它们:
- 别名 bug(aliasing bug):两个变量指向同一个可变对象,一处修改意外影响另一处。
- 差一 bug(off-by-one):循环或索引边界多一或少一。
- 顺序 bug(ordering):操作的执行顺序与预期不符。
- 代码克隆 bug(code-clone):复制粘贴的代码只改了一处,遗漏了其他副本。
- 遍历时修改 bug(mutating-while-iterating):在迭代集合的同时修改它,导致未定义行为。
第一道防线:让 bug 不可能出现
最强大的调试,是在 bug 写出来之前就消灭它。6.102 提供三种机制:
- 静态检查(static checking):编译器在编译时捕捉类型错误和不合法操作。TypeScript 的类型系统就是一种静态检查。
- 动态检查(dynamic checking):运行时检查,比如数组越界、空指针、类型强转失败。
- 不变性(immutability):使用不可变类型和常量,让对象一旦创建就不能被修改,从根本上消除别名和状态混乱。
// 用不变性消除别名 bug
const original: readonly number[] = [1, 2, 3];
// original.push(4); // 编译错误:无法修改只读数组
第二道防线:让 bug 容易定位
当无法完全避免 bug 时,要让它们尽早暴露:
- 快速失败(fail fast):一旦发现异常立即抛出,而不是带着错误状态继续运行。
- 断言(assertion):在代码中明确记录并强制执行你的假设。
- 增量开发:每次只加一小部分功能,立即测试,缩小出错范围。
- 模块化与封装:把错误限制在局部模块内。
- 最小化变量作用域:变量越局部,越难被错误地复用。
function sqrt(x: number): number {
console.assert(x >= 0, "sqrt 的参数必须非负");
return Math.sqrt(x);
}
复现 bug
定位 bug 的第一步是找到一个小的、可重复的测试用例,能稳定触发失败。如果 bug 时有时无,先不要急着改代码,而是缩小输入、固定随机种子,直到失败变成必然。
用科学方法定位 bug
调试本质上是一个循环的科学实验:
- 研究数据:观察输入、输出、错误信息、日志。
- 提出假设:对 bug 原因给出一个可验证的解释。
- 设计实验:用探针验证或推翻假设。
- 重复:直到定位根因。
探针工具
- 断言:在关键点检查不变量。
- 打印语句:输出变量值和程序路径(用完记得删)。
- 调试器:设置断点、单步执行、检查调用栈。
delta 调试
Delta 调试的思路是对比成功运行与失败运行的差异,逐步缩小导致失败的最小输入或最小代码变更。这是一种系统化的二分搜索。
修复 bug
找到根因后,修复应该是谨慎而完整的:
- 检查相关 bug:同一处代码可能还有同类问题。
- 撤掉调试探针:删除临时的打印和断点。 编写回归测试:让修复被测试覆盖,防止日后复发。
- Add, Commit, Push, Reflect:提交代码并反思这次 bug 的教训。
其他调试技巧
- 换个视角:向同伴(甚至橡皮鸭)讲解你的代码——「橡皮鸭调试法」。
- 书面描述:把问题写下来,往往能在叙述中自己发现矛盾。
- 寻求帮助:卡住太久时及时提问。
- 先睡一觉:大脑会在后台继续处理难题。
关键要点
- 预防静态检查 + 动态检查 + 不变性,让 bug 不可能出现。
- 暴露快速失败、断言、增量开发、模块化,让 bug 尽早暴露。
- 复现找到最小可重复的测试用例。
- 科学研究数据 → 假设 → 实验 → 重复,系统定位根因。
- 修复撤探针、写回归测试、提交并反思。