变量角色与两层理解 — 从「看得懂行」到「看得懂意图」
这一章讲两件事: 一套给变量归位的速查表(11 种角色),以及比它更深一层的区分—— 「看懂了每一行」和「看懂了作者想干什么」是两种不同的看懂。 前者治第 01 章说的「缺知识」,后者治的是读完了还不知道代码想干嘛的那种空。
1. 全景:理解的两个台阶
一段陌生代码
├─ 台阶一:变量各自在干什么?──变量角色框架(11 种角色)
│ 「counter 是个计数器,max_prime_factors 存的是目前最大值」
│ 解决的是局部的、逐个元素的困惑
└─ 台阶二:整段代码想干什么?──两层理解模型
文本结构知识(看得懂行)≠ 计划知识(看得懂意图)
靠焦点起步 → 顺藤摸瓜 → 归纳出概念
图说:两个台阶是递进的——角色是砖,意图是房子。
2. 变量角色框架:11 种角色一网打尽
变量难懂的病根,东芬兰 大学的萨雅涅米(Sajaniemi)诊断得很准:大多数人的长时记忆里,没有和变量配套的图式——图式就是大脑里绑定成套的知识单元(比如「去餐厅怎么点单」是一整套流程,记的时候是一块、不是一串零散动作),缺了变量这一套,读变量就只能逐个现场琢磨1。他还观察到,程序员给变量起的名字,信息量不是太空(「variable」「integer」)就是太具体(「number_of_customers」),两头都不顶用。于是他绕开命名,直接给变量的行为分类:一个变量在程序里承担的任务,就叫它的角色。
这套框架声称 11 种角色就能罩住绝大多数变量2:
| 角色 | 一句话 | 典型例子 |
|---|---|---|
| 固定值 | 赋一次就不变 | 圆周率 π、配置项 |
| 步进器 | 按可预测的节奏逐个走值 | for 循环的 i |
| 标志 | 报告「发生了没」 | is_error |
| 步行器 | 在结构里游走,走到哪算哪 | 链表里找插入点的指针(存「下一个元素在哪」而不是元素本身的值) |
| 最近持有器 | 攥着最新一个值 | line = file.readline() |
| 最佳持有器 | 攥着目前最想要的值 | 当前最大/最小值 |
| 收集器 | 边走边攒总数 | sum += list[i] |
| 容器 | 装一堆可增删的东西 | 列表、栈、树 |
| 跟随器 | 总跟着另一个变量的上一步 | 遍历链表时的「前一个」指针 |
| 组织器 | 只做重新摆放的临时工 | 排序前的暂存 |
| 临时 | 用完就扔 | swap 里的 temp |
角色和编程范式无关——面向对象的类属性照样归位:一个 Dog 类里,name 出生后不变是固定值,age 每次 birthday 加一,是步进器3。
3. 主走查:给一段程序「点名」
拿书里的例子走一遍。这段 Python 程序统计某个范围内质因数最多的是哪个数:
upperbound = int(input('Upper bound?'))
max_prime_factors = 0
for counter in range(upperbound):
factors = prime_factors(counter)
if factors > max_prime_factors:
max_prime_factors = factors
只看名字,你能猜个大概;按角色框架逐个点名,四个变量的行为模式立刻清楚了4:
| 变量 | 角色 | 判定依据(行为,不是名字) |
|---|---|---|
| upperbound | 固定值 | 输入之后再也不变 |
| counter | 步进器 | 循环里按 1 递增,节奏可预测 |
| factors | 最近持有器 | 每轮攥着「当前这个数」的质因数个数 |
| max_prime_factors | 最佳持有器 | 只在打破纪录时才换值 |
点名完毕,程序的结构自动浮出来:一个步进器驱动、一个最近持有器做单步计算、一个最佳持有器收集冠军——这是一切「扫描求最值」程序的通用骨架。研究证实这套框架真能帮新手:用过它的学生读代码表现更好,原因正是这种「看角色识程序」——见到步进器加最佳持有器的组合,基本可以断定这是个搜索程序5。
两个落地用法:打印代码、给 11 种角色各配一个小图标,边读边标;写代码时把角色名嵌进变量名(比如 max_ 前缀就在喊「最佳持有器」)6。
4. 一段命名法的冤案:匈牙利命名法本来是对的
把类型写进变量名——strName 表示字符串、lDistance 表示长整数——这套规矩叫匈牙利命名法,今天基本被当成反面教材。书里给它翻了一半的案,这段考古值得看,因为它和变量角色框架直接相关7。
匈牙利命名法是西姆尼(Simonyi)1976 年在博士论文里提出的(他后来在微软主持 Word 和 Excel)。今天流行的「类型前缀」版本,其实不是他的原意8:
- 系统型(今天的通行版,也是被嫌弃的版本):前缀标类型,
strName。在 IDE 能随时显示类型的今天,它只剩下让名字变长这一个作用; - 应用型(原始版):前缀标语义(即这个名字表明它是什么意思,而不是它属于什么类型)。
rw/col区分「行」和「列」——两者都是整数,但搞混了就出 bug;cColors的 c 表示「数量」。
怎么讹传的?书里给出了主流解释(斯波尔斯基的说法:西姆尼论文里用词是「种类」被理解成「类型」)和作者自己的猜测(几个程序员用错后以讹传讹,再经一本 Windows 编程畅销书固化)9。作者的观点是:应用型匈牙利和变量角色框架精神相通——t 前缀表临时、min/max 前缀表最佳持有器,都是把角色写进名字10。被判死刑的应该是系统型,不是这套思路。
5. 两层理解:看得懂行,不等于看得懂意图
角色框架把每个变量讲清楚了,但这只到「台阶一」。佩宁顿(Pennington)把程序员对代码的理解分成两层11:
- 文本结构知识:对代码表面的理解——认识关键字、知道每个变量扮演什么。到这一层,你知道「每行在干嘛」;
- 计划知识:对代码编写者意图的理解——这些零件组合起来要实现什么目标。到这一层,你才知道「这段代码为什么存在」。
两层的落差有个精确的刻画。西利托(Sillito)观察 25 位程序员读代码,把理解过程总结成四步,起点是找焦点——开始阅读的立足点12:
- 找焦点:入口(main 方法、onLoad),或出错的那行、被性能工具点名的那行;
- 以焦点为圆心扩圈:查它用到什么、被谁调用,一圈圈扩大;
- 聚实体成概念:把相关的变量、方法、类归拢成「这大概是个什么机制」;
- 理概念间的关系:理解多个概念怎么配合成整个程序。
陷阱在第 2 步就能现形:有些框架里焦点相距十万八千里(依赖注入式的框架尤其如此),每行都看得懂,却始终拼不出「实际运行时谁调用谁」——这就是典型的有文本结构、缺计划。中招的人会陷入一种特定的自我怀疑:「代码又不难,我怎么就是不懂?」——不是你的问题,是焦点离得太远13。
6. 四步落地:从焦点到可以交差的笔记
把西利托的抽象四步配上具体操作,就得到一套可执行的深读流程14:
- 找焦点:main,或这次任务直接相关的位置;
- 圈切片:把和焦点相关的代码连起来——行 X 的切片就是与 X 相关的全部代码行,圈完它你就知道「这一摊」有多大;
- 认调用模式:切片里反复被调用的方法往往就是关键角色,值得单独深挖;不相关的方法先移出视线;
- 归纳概念:列出用到的数据结构和它们的约束(这棵树是不是二叉?能不能随便插节点?),整理成实体表和概念表。
最后那张表别扔——它就是这段代码的文档,给下一个人省一遍第 1~3 步15。第 04 章的依赖图六步,本质上是这套流程在「变量/调用/类」三个层面的机械版;这里补上的,是它缺失的起点(焦点)和终点(概念)。
7. 作者的判断与证据
**有实验撑着的:**角色框架对新手的助益(萨雅涅米的教学研究);西利托的四步模型(25 位程序员的实地观察);两层理解(佩宁顿的模型研究)。
作者的发挥,要分开看:「给角色配图标」是作者个人的实践偏好;「变量名里嵌角色名」是建议而非数据;匈牙利命名法「以讹传讹」的成因,书里同时给出斯波尔斯基的流行解释和作者自己的猜测,并明说后者是推测。
**书的坦白:**框架只覆盖「绝大多数」变量,总有归不进 11 类的;角色能识别「搜索程序」这类典型骨架,但不提供任何程序正确性上的保证。
8. 边界与局限
- 角色框架描述的是过程式代码的变量行为,对一个大型面向对象系统逐个标角色,性价比会急剧下降——它最适合的是读函数和小模块;
- 「焦点相距甚远」的框架痛点(依赖注入、事件驱动)在第 13 章的体检表里还会以「隐藏的依赖关系」的面目出现—— 那里给的是系统级的度量,这里给的是个人的应对;
- 计划知识的获取没有机械流程:四步能保证「走完」,不保证「走通」——猜错了意图,实体表会跟着错,所以第 4 步的产物要当假设而不是结论。
9. 可带走的
- 读变量先问「它是步进器还是持有器」,行为模式比名字可靠;
- 见到「步进器+最佳持有器」,先假设这是个扫描求最值/搜索程序,再验证;
- 给团队的新代码审查加一条:关键变量名里带上角色线索(如 max_ 前缀);
- 别全盘照骂匈牙利命名法——被骂的是类型前缀,语义前缀的思路还活着;
- 「每行都懂但整体不懂」不是智力问题,是焦点太远——先找个焦点,再扩圈;
- 深读产出实体表和概念表,直接当文档提交;
- 圈切片是治「来回跳转迷路」的特效药。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 框架的动机 | 5.1 变量角色框架 | text/30-ch05-01-5-1.txt:7(搜「变量角色框架」) |
| 四变量点名 | 5.1 变量角色框架 | text/30-ch05-01-5-1.txt:24(搜「角色是固定值」) |
| 11 种角色 | 5.1 变量角色框架 | text/30-ch05-01-5-1.txt:28(搜「11种角色」) · text/30-ch05-01-5-1.txt:30(搜「固定值(fixed-value)」) · text/30-ch05-01-5-1.txt:32(搜「步进器(stepper)」) · text/30-ch05-01-5-1.txt:56(搜「临时(temporary)」) |
| Dog 类与角色跨范式 | 5.2 角色和范式 | text/31-ch05-02-5-2.txt:19(搜「步进器」) |
| 新手受益、识别搜索程序 | 5.2 角色和范式 | text/31-ch05-02-5-2.txt:25(搜「搜索程序」) |
| 图标与命名建议 | 5.2 角色和范式 | text/31-ch05-02-5-2.txt:53(搜「设计一套对应的图标」) · text/31-ch05-02-5-2.txt:61(搜「角色名称」) |
| 匈牙利命名法考古 | 5.2 角色和范式 | text/31-ch05-02-5-2.txt:65(搜「匈牙利命名法」) · text/31-ch05-02-5-2.txt:75(搜「应用型匈牙利命名法」) · text/31-ch05-02-5-2.txt:79(搜「以讹传讹」) · text/31-ch05-02-5-2.txt:81(搜「最佳持有器不谋而合」) |
| 两层理解 | 5.3 加深对程序的了解 | text/32-ch05-03-5-3.txt:9(搜「Pennington」) · text/32-ch05-03-5-3.txt:11(搜「文本结构知识代表」) |
| 焦点与四步 | 5.3 加深对程序的了解 | text/32-ch05-03-5-3.txt:17(搜「焦点(focal point)」) · text/32-ch05-03-5-3.txt:25(搜「寻找焦点」) |
| 焦点相距甚远 | 5.3 加深对程序的了解 | text/32-ch05-03-5-3.txt:33(搜「相距甚远」) · text/32-ch05-03-5-3.txt:35(搜「缺乏计划知识」) |
| 代码切片 | 5.3 加深对程序的了解 | text/32-ch05-03-5-3.txt:65(搜「切片定义为」) |
| 表作文档 | 5.3 加深对程序的了解 | text/32-ch05-03-5-3.txt:79(搜「重要资料」) |
| 没 人读代码的背景 | 5.4 阅读代码和阅读文本有相似之处 | text/33-ch05-04-5-4.txt:5(搜「高德纳」) · text/33-ch05-04-5-4.txt:7(搜「自己写更容易」) |