组块 — 老手为什么读得快
这一章讲三件事: 为什么你记不住刚看过的代码;国际象棋大师的秘密为什么就是程序员的秘密; 以及「好读的代码」在认知层面到底好在哪。 上一章说短时记忆坑位少,这一章回答由此逼出的第一个问题:坑这么少,人怎么还能处理代码这种动辄几十行的东西?
1. 全景:一条从「装不下」到「压得下」的路
代码涌进眼睛 → 先落进一个超大的「缓冲区」(图像记忆)
→ 只有一小部分被挑进短时记忆(2~6 个坑位)
→ 靠长时记忆的存货把信息「打包」成块
→ 一块只占一个坑位,于是装得下了
图说:本章的所有机制都长在这条流水线上——打包叫组块,
「挑哪些」靠长时记忆的存货,而缓冲区是大多数人不知道的第一站。
先补一个背景数字:研究发现程序员平均每天有接近六成时间花在读代码而不是写代码上1。读代码既然是主业,这条流水线的效率就是编程效率的大头。
2. 先亲手装不下一次:默写实验
书里设计了一个简单粗暴的实验:看一段 Java 代码三分钟,盖上,凭记忆默写。第一段代码是插入排序(算法就是把一套解题步骤写死、机器照着一步步执行的那种程序;插入排序是把一列数字从小到大排好的步骤)2。
默写时大脑分头进货:
for (int i = 0; i < array.length; i++)这类固定写法——从长时记忆取,因为熟;- 数组里那 12 个具体数字——从短时记忆取,因为刚看;
- 「这程序是实现插入排序的」——也从长时记忆取,并且能反哺:忘了交换两个元素怎么写时,「插入排序应该有交换」这个知识会帮你把空白补上3。
很多程序员默写时还会自己加注释,先写「输出数组」再填代码——注释在这里是记忆的脚手架4。
第二段默写实验才是真正的试剂:书里选了一段结构相似但你看不出用途的代码,还故意把循环变量起名叫 b 和 l(字母 l 和数字 1 几乎无法区分)5。这次几乎所有人都不及格。同一个人,两段代码,表现天差地别——变的不是脑子,是脑子里的存货。
为什么默写必然失败?短时记忆的两个硬参数:信息在里面的存活时间不超过 30 秒6;容量方面,1956 年米勒那篇著名论文说 7±2,近年研究把这个数砍到大约 2~6 个信息元素7。你不是记性差,是坑位本来就只有几个。
3. 国际象棋大师的秘密:组块
坑位这么少,人怎么还能记住 32 个棋子的棋局?荷兰数学家德格鲁特(de Groot)用两组实验给出了答案8。
**实验一:**给职业棋手和普通棋手看几秒真实对局,盖上,还原。职业棋手远胜。
**实验二:**同样流程,但棋子是胡乱摆的——不是任何真实对局。这次两组表现一样烂9。
第二组实验是整个发现的关键。如果大师赢在「记性好」,乱摆的棋子应该赢得更轻松才对(纯记数字、记位置的事)。他们没赢,说明他们记忆真实棋局时用的根本不 是「记棋子」这条路。
普通棋手走的确实是「记棋子」的路:心里默念「车在 a7,兵在 b5,王在 c8……」——每个棋子占一个坑,几个坑位瞬间烧完10。
主走查:大师脑子里那一秒钟发生了什么
拿实验一里大师的原话当走查对象。他说出的记忆内容是:
「西西里防御开局,但马的位置左移两格。」
这句话在大师脑内的加工过程,一步一步是:
看到棋局
↓ 长时记忆里存着成百上千个「开局样子」,棋局一比对就命中了一个:
「这是西西里防御」 ← 一次识别,占 1 个坑
↓ 在已识别的开局之上找差异:
「马」的位置和标准开局不同 ← 占 1 个坑
↓ 差异是什么:
「左移」「两格」 ← 各占 1 个坑
↓ 汇总:手里一共 4 个信息元素(「西西里防御」「马」「两格」「左」)
短时记忆有 2~6 个坑 → 装得下,齐活
32 个棋子被压成了 4 个元素——每个坑里塞的是一坨,不是一个。这种「把一坨信息打包成一块、一块只占一个坑」的东西,就叫组块(chunk)11。大师不是记力惊人,是存货惊人:长时记忆里存的开局越多,能打包的素材越多。
判断(我们的,不是书里的): 组块是这个领域最被低估的「贫富差距」。两位智商相当的程序员读同一段代码,一位把
for循环看成一个字,另一位把它看成「一个循环」一个字——他们消耗的坑位数差了好几倍,表现差距全从这里来。所以「读不懂」的第一反应不该是「我笨」,该是「这块我还没有存货」。 如果错,会错在: 如果某个领域真有人能不靠组块、纯靠扩大短时记忆取胜——但书里明说科学家没找到可靠扩大短时记忆的办法12,所以这条判断的败点目前找不到。
4. 搬到代码上:McKeithen 的程序员实验
棋手的结果能搬到代码上吗?1981 年贝尔实验室的麦基森(McKeithen)照方抓药:找来 53 位初级、中级、高级程序员,读 30 行的 ALGOL 程序(一门今天已少用的早期语言),两分钟后默写。一半被试读到的是正常程序,另一半读到的程序代码行被随机打乱——相当于「乱摆的棋子」13。
结果和棋手实验严丝合缝:
| 程序版本 | 谁记得多 |
|---|---|
| 正常程序 | 高级 > 中级 > 初级,差距明显 |
| 打乱行序的程序 | 三组几乎一样差 |
打乱的程序没法组块——就像乱摆的棋局,老手的存货全部失效。这项研究还有个对带团队很要紧的推论:新手能加工的代码量远少于老手,哪怕这位新手很聪明、别的语言很熟15。给新人安排任务时的期望值,要按这个事实校准。
还有一层更细的证据:让三组程序员记 21 个 ALGOL 关键字,新手靠编句子硬记(「TRUE IS REAL THEN FALSE」),老手靠编 程知识分组(TRUE 和 FALSE 一组、IF/THEN/ELSE 一组)16。分块(组块的动词形式:把信息打包成块的动作)连记单词时都在发生,老手打包的收纳方式就是这行知识的原生结构。
到这里,「检索」这个词该正式登场了:大脑从长时记忆里回头查找存货的动作,就叫检索。组块能不能打起来,取决于检索能不能命中——存货里没有西西里防御,棋局摆在那也白搭。
5. 被跳过的一站:图像记忆
上一章的流水线少了一站。信息不是从眼睛直接蹦进短时记忆的——中间还有一个容量大得多的中转站。
荷兰心理学家斯珀林(Sperling)的实验逼出了它17:给被试看 3×3 或 3×4 的字母网格,只闪 50 毫秒(人眼反应都要 200 毫秒,根本来不及逐个看)。然后随机指定一行或一列让被试复述——75% 的测试里被试全对。可要是让他们复述全部字母,就只能说出一半左右。
只考一行时全对,说明整个网格都曾被完整记录;全考时记不全,说明短时记忆只装得下一部分。斯珀林的结论:视觉信息先整个进一个超大但超短暂的缓冲区(先整个收下来、转眼就清空的中转站),他称之为图像记忆——书里拿挥烟花棒做比:火光划出的字之所以能被看 见,就是它短暂地留在了这个缓冲区里18。
对读代码的含义很直接:扫一眼代码,你其实已经「看到」了远比短时记忆容量多的东西——结构、嵌套深浅、留白。这些信息大部分没被挑进短时记忆,但你可以主动去捞:书里建议的练习是扫视几秒后闭眼回忆「代码长什么样、哪里深哪里浅」,捞回的部分正是后续加工的原材料19。
6. 让代码「好压」:设计模式、注释、信标
组块靠存货,那写代码的人能做什么?书里给出三个被实验支持的办法,它们都在帮读的人少占坑。
**办法一:用设计模式。**设计模式是编程圈通行的几种标准结构套路(比如「观察者模式」:一个东西变了,登记过要听消息的东西自动收到消息)。蒂希(Tichy)做了两轮实验——先以学生、后以专业程序员为对象——先测他们改代码的速度,再教一轮设计模式课程,换个代码库再测:课程之后,改「用了设计模式的代码」明显变快,改「没用设计模式的代码」变化不大20。套路认得了,整段代码压成一个组块。
**办法二:写注释,但写对层次。**读带注释的代码更费时——这不是坏事,恰恰说明读的人把注释也读了进去21。关键在层次:法恩(Fan)的博士论文发现,高级注释(「该函数按顺序输出给定的二叉树」)能帮人把一大段代码打包成块;低级注释(在 i++ 后面写「自增 i」)反而碍事——它把已经是一个组块的东西又切碎了22。
**办法三:放信标。**信标(beacon)是代码里能让读的人「突然明白这代码在干嘛」的元素——一个贴切的名字、一句点题的注释23。拿书里的二叉树中序遍历例子走一遍:初读不知道代码干嘛,看到注释里有「树」和变量 root,形成「跟树有关」的假设;再看到 left 和 right 两个属性名,假设收窄为「二叉树」——每撞到一个信标,假设范围缩一圈24。信标分两种:简单信标是单个自解释的元素(root、加号这类运算符),复合信标是几个元素合起来才表意(self.left 和 self.right 合看才知道是二叉树;一个完整 for 循环也是复合信标)25。研究还发现老手读代码时用信标的频率远高于新手26——信标是老手组块的原材料,写代码时多放信标,等于给未来的读者递存货。
7. 默写代码:最便宜的自我体检
这一章的所有实验用的是同一个动作——读一段、盖上、默写。书里把它改造成个人工具:默写代码可以当体检用。
原理回到组块:大脑记得住的部分,就是已经打得出包的部分。默写一段你自认为熟悉的代码,卡壳的地方精确暴露你的存货缺口——是语法不熟?设计模式没认出来?还是领域概念缺失?对照原文一看便知27。操作上配合刻意练习(小步、重复、针对单一技能的练习法,像音阶之于琴童)做:哪块默不出来,补哪块,过阵子再默28。
8. 作者的判断与证据
**有实验撑着的:**短时记忆 2~6 个元素(米勒及后续研究);棋手两项实验(德 格鲁特);程序员默写实验(麦基森,53 人);设计模式两轮实验(蒂希,学生+专业人士);注释层次(法恩,博士论文);信标使用频率(克罗斯比)。样例效应这一章之后还会重现——组块研究是全书实验密度最高的部分。
作者的推测,要分开看:「扫视代码可以主动利用图像记忆」是作者从斯珀林实验引申的练习建议,斯珀林本人测的是字母网格,没测过代码;「写代码时应多放信标」是从信标研究引申出的实践主张,方向有据,具体到「放多少」没有数据。
**书的坦白:**短时记忆容量的确切数字学界有争议(7±2 vs 2~6),科学家也没找到可靠扩大短时记忆容量的办法——所有改善都发生在「打包」那一端,不在「扩容」这一端。
9. 边界与局限
- 麦基森实验是 1981 年、ALGOL 时代的。今天代码的排版、命名、IDE 辅助都变了,「新手能加工的少得多」这个方向不会错,差距的具体幅度别照搬。
- 设计模式的收益不均匀:蒂希的数据里,观察者模式带来的提速比装饰器模式明显29。「用了模式就好读」不是逐模式成立的。
- 图像记忆的代码应用是引申:扫视有用有眼动研究旁证(第 06 章会讲),但「闭眼回忆」这个练习形式本身是作者设计的,效果因人而异。
- 本章全是「读」侧的机制。组块解释了老手为什么读得快,但没解释知识怎么存进去——那是第 03 章的事。
10. 可带走的
- 卡壳时先问「这块我有没有存货」——没有存货的代码,看多久都装不进短时记忆;
- 读不出用途的代码比读得出用途的难记一个量级,陌生代码先弄清「它在干嘛」再逐行读;
- 老手强在组块不在记性——所以「多看代码攒存货」是可执行的练法,「练记性」不是;
- 给新人安排任务前,记住新手能加工的代码量远少于老手;
- 注释写高的不写低的:一段代码一句「它在干嘛」,别给
i++配解说; - 变量名、注释就是给未来读者的信标,起名时想想「这名字能帮人打包吗」;
- 默写一段半页长的代码是最便宜的存货体检,卡壳处即缺口处;
- 扫一眼再看一眼:第一眼的印象是真的存在过的,值得主动去捞。
11. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 六成时间在读代码 | 第2章 快速阅读代码 | text/13-ch02.txt:21(搜「接近60%」) |
| 默写练习、插入排序 | 2.1 快速阅读代码 | text/14-ch02-01-2-1.txt:13(搜「不要超过3分钟」) · text/14-ch02-01-2-1.txt:42(搜「遍历数组的for循环」) |
| 默写时加注释 | 2.1 快速阅读代码 | text/14-ch02-01-2-1.txt:52(搜「添加注释以帮助记忆」) |
| b 和 l 变量名 | 2.1 快速阅读代码 | text/14-ch02-01-2-1.txt:85(搜「非常像数字1」) |
| 30 秒、7±2、2~6 | 2.1 快速阅读代码 | text/14-ch02-01-2-1.txt:91(搜「不超过30秒」) · text/14-ch02-01-2-1.txt:95(搜「神奇的数字7」) · text/14-ch02-01-2-1.txt:97(搜「2~6个信息元素」) |
| 德格鲁特两项实验 | 2.2 弥补记忆容量不足的短板 | text/15-ch02-02-2-2.txt:13(搜「率先提出组块」) · text/15-ch02-02-2-2.txt:21(搜「胡乱摆放」) |
| 逐子记忆与 4 个信息元素 | 2.2 弥补记忆容量不足的短板 | text/15-ch02-02-2-2.txt:23(搜「车在a7」) · text/15-ch02-02-2-2.txt:25(搜「4个信息元素」) |
| 组块定义 | 2.2 弥补记忆容量不足的短板 | text/15-ch02-02-2-2.txt:31(搜「称为“组块”」) |
| 容量没法扩大 | 2.2 弥补记忆容量不足的短板 | text/15-ch02-02-2-2.txt:31(搜「无法再依靠长时记忆」) · text/14-ch02-01-2-1.txt:97(搜「尚未找到」) |
| McKeithen 程序员实验 | 2.2 弥补记忆容量不足的短板 | text/15-ch02-02-2-2.txt:63(搜「53位」) · text/15-ch02-02-2-2.txt:65(搜「几乎没有区别」) · text/15-ch02-02-2-2.txt:69(搜「少得多」) |
| 关键字记忆实验 | 2.3 看到的代码比读到的代码多 | text/16-ch02-03-2-3.txt:59(搜「TRUE IS REAL」) |
| 图像记忆与 Sperling | 2.3 看到的代码比读到的代码多 | text/16-ch02-03-2-3.txt:19(搜「50毫秒」) · text/16-ch02-03-2-3.txt:21(搜「图像记忆」) · text/16-ch02-03-2-3.txt:13(搜「烟火棒」) |
| 扫视代码练习 | 2.3 看到的代码比读到的代码多 | text/16-ch02-03-2-3.txt:27(搜「扫一眼代码」) |
| 设计模式实验 | 2.3 看到的代码比读到的代码多 | text/16-ch02-03-2-3.txt:69(搜「设计模式文档」) · text/16-ch02-03-2-3.txt:73(搜「明显加快」) |
| 注释的两面 | 2.3 看到的代码比读到的代码多 | text/16-ch02-03-2-3.txt:81(搜「更关心注释」) · text/16-ch02-03-2-3.txt:83(搜「不利于分块」) |
| 信标定义与例子 | 2.3 看到的代码比读到的代码多 | text/16-ch02-03-2-3.txt:87(搜「茅塞顿开」) · text/16-ch02-03-2-3.txt:125(搜「缩小假设范围」) · text/16-ch02-03-2-3.txt:129(搜「简单信标」) · text/16-ch02-03-2-3.txt:131(搜「复合信标」) |
| 老手更依赖信标 | 2.3 看到的代码比读到的代码多 | text/16-ch02-03-2-3.txt:135(搜「频繁使用信标」) |
| 刻意练习、默写当体检 | 2.3 看到的代码比读到的代码多 | text/16-ch02-03-2-3.txt:179(搜「刻意练习」) · text/16-ch02-03-2-3.txt:183(搜「帮助你评估自己对哪些概念」) |