跳到主要内容

认知负荷 — 过载从哪来,怎么给大脑减负

这一章讲三件事: 工作记忆的过载为什么值得单独命名(认知负荷);过载里有哪几部分是代码白塞给你的; 以及四件立刻能用的减负工具。它是第 01 章埋下的第三个困惑——「缺加工能力」——的完整展开。

1. 工作记忆的正式定义:用在哪,比是什么重要

第 01 章把三套记忆比作硬盘/内存/处理器,这里要先把「处理器」说准,因为学界对它的边界有分歧:有人认为工作记忆就是短时记忆的别名,有人认为两者是两回事。作者的立场是分开,并给出了一个重实用的定义1:

工作记忆就是「用在某个问题上的短时记忆」。

区别用一个对照看最清楚:记住一个电话号码,是短时记忆的活;边记电话号码边算「它除以 7 等于几」,起作用的才是工作记忆2一个只管存,一个拿着存的东西做推理。既然是短时记忆在干活,它就继承同一个容量上限——一次大约加工 2~6 个元素。这个「一次能加工多少」的能力,术语叫认知负荷;要加工的东西超过容量,就叫过载3

2. 过载的成分:内部、外部,以及预告第三种

认知负荷理论由澳大利亚心理学家斯威勒(Sweller)提出,他把负荷拆成三种4。本章用前两种,第三种(关联认知负荷)留到第 11 章——它管的是「学习」本身。

**内部认知负荷是问题本身自带的。**用勾股定理解直角三角形,先平方、再相加、再开方,这三步是问题固有的,谁都省不掉,也改不掉5。程序设计里管这叫固有复杂性。

**外部认知负荷是表达方式白加的。**同一个三角形题,一种问法直接给「两条边长 8 和 6」;另一种问法说「边 a 长 8、边 b 长 6」——数学难度一模一样,但后一种逼你多做一道工序:把 a 和 8、b 和 6 各自对上号。多出来的这部分负担,不来自问题,来自问法6。程序设计里管这叫偶发复杂性:问题没变,是代码的写法把它变重了。

外部负荷还有一个要紧的性质:因人而异。两段计算完全等价的 Python 代码——一行方括号的列表推导式,和四行 for 循环——内部负荷相同;但熟悉列表推导式的人读前者轻松,不熟悉的人读前者反而更重7「哪段代码好读」没有绝对答案,取决于读的人。

自检信号还是第 01 章那条:读代码想拿笔记点什么、想一步步跟——负荷已经高了。这时先分诊:哪些负担是问题自带的(内部,躲不掉),哪些是表达白塞的(外部,可减)。下面两节全是减外部负荷的手段。

3. 主走查:同一道题的两次加压

把第 2 节的三角形走成一条完整的对照,看外部负荷是怎么一层层叠上去的:

版本 A(低压):「直角三角形,两条直角边长 8 和 6,求斜边。」
大脑要端着:8、6、公式 → 3 个元素,从容

版本 B(加压一层):「直角三角形,边 a = 8,边 b = 6,求斜边。」
大脑要端着:a↔8、b↔6、公式 → 5 个元素,开始挤

版本 C(再加一层,代码版):
def hyp(a, b): return (a**2 + b**2) ** 0.5
r = hypotenuse(x=8, y=6)
大脑要端着:调用处传的 x/y 和定义处的 a/b 对不上号
(**参数**就是函数收到的一批输入值,这里连名字都不一致)、
「hyp」和「hypotenuse」是不是同一个函数、8 和 6 谁是底谁高 → 已经超 6 个

图说:数学难度三版完全相同,「同时端着的东西」每版都在涨。
版本 B、C 里涨出来的都是外部认知负荷。(代码是本拆解为演示补写的,
书里的演示是图形版;参数就是函数收到的一批输入值。)

这条例子解释了一类日常现象:为什么同一份代码,写的人觉得清楚、读的人觉得费劲——写的人早把 a↔8 的对应关系焊死在脑子里(进了长时记忆),读的人却要在工作记忆里现摆。

4. 减负工具一:认知重构——为「今天能读懂」而改代码

大多数程序员认识重构(不改变代码行为、只改进内部结构的改动,比如把重复代码收进一个函数),它的目标是长期好维护。但好维护不等于好读:逻辑全拆进小函数后,读的人得在多个文件之间来回跳,屏幕上滚来滚去——维护性上赚的,阅读时又赔回去8

所以书里立了一个新词:认知重构——同样不改行为,但目标只有一个:让「此刻的我」读得懂。它和常规重构有三点不同9:

常规重构认知重构
为谁改未来的维护者此刻正在读的你
方向通常拆小、收拢怎么好读怎么来,甚至反向
寿命长期保留临时货,读懂即可回滚

「反向」是指反向重构:把小函数内联(把函数体直接展开到调用它的位置)回去。方法名叫 calculate() 这种说了等于没说的,内联展开反而一眼看懂10。配合手段:把方法声明挪到第一次调用的旁边、跳转功能能用但每次跳转也耗工作记忆11

换掉不熟悉的语法是认知重构的另一大类。看不懂 lambda 表达式(不需要起名字的 inline 函数)塞在 filter() 里,就改写成普通命名函数传进去;读不动列表推导式,就展开成 for 循环;三元运算符(把 if/else 压成一行的写法)费劲,就还原成正经 if/else12。书里对犹豫者有一句关键的宽慰:代码可读性取决于读者的先验知识,把代码改成自己更熟悉的形式来帮助理解,并不丢人13

工程上这从来不难:开个本地「代码理解」分支随便改,读懂了就丢;个别改动有长期价值再合回去14。还有一个二段式技巧:给这类「高级语法」做抽认卡时,卡片两面都写代码——一面普通写法,一面高级写法——把「看不懂→翻译」的对照直接焊进存货,以后就不必再改了15

5. 减负工具二:依赖图——代码结构画出来

认知重构改代码,依赖图改读法。适用场景:代码结构性强、你不知道该按什么顺序读、来回跳转跳到迷路。

操作是六步,全程拿打印稿或 PDF 做记号16:

  1. 圈出所有变量;
  2. 把同名变量出现的位置两两连线——从此找一个变量的下落不用再靠滚动和 Ctrl+F;
  3. 用第二种颜色圈出所有方法/函数调用(调用,即执行到这行时跳去运行那个函数);
  4. 把每个调用和它的声明连线——只被调用一次的方法要特别标出,它们就是第 4 节内联的候选;
  5. 用第三种颜色圈出所有类的实例;
  6. 把实例和类声明连线。

画完之后,这张图就是代码的「地图」:从入口(比如 main 方法)开始读,每遇到一个调用或实例,顺着线直接跳到该去的位置17。它减负的原理和状态表一样:把「反复查找」这件苦役从工作记忆卸载给一张纸。查找不产生理解,它只是烧坑位。

6. 减负工具三:状态表——计算过程列出来

代码有两种难:结构难(用依赖图),计算难——变量咬着变量、一步喂一步,读的时候脑子跟不上变量的值翻新。这种时候用状态表:每个变量占一列,每个执行步骤占一行,把值一格一格填进去18

眼熟吗?第 01 章主走查里那张 BASIC 追踪表,就是一张状态表——N、N2、N1、B$ 各占一列,循环每转一轮占一行。当时它回答「工作记忆为什么装不下」,现在它有了名字和用法。

填表的几条纪律:每一行代表一步执行(一次迭代,即循环走一圈;一个分支算一行);逐格填满,不要跳格——只填你关心的变量恰恰是过载的根源,填满之后再次阅读时就可以只看表、不用再算19。书里还提到一个自动化替身:Python Tutor 这类在线工具能把程序的每一步执行状态可视化,尤其适合调试,不过它存列表时展示的引用关系需要适应一下20

两个工具的分工一句话:依赖图管「代码怎么组织」,状态表管「代码算出了什么」。拿到一段陌生代码,先画图理结构,再列表跟计算,两头都清了才开始动它21

7. 作者的判断与证据

**有实验或成熟理论撑着的:**认知负荷三分法(斯威勒的认知负荷理论,教育心理学主流);「外部负荷因人而异」(第 02 章列表推导式与新手研究同源);Python Tutor 有独立研究支持其对理解的助益(学生需要适应期)。

作者的概念创新,要认账:「认知重构」「反向重构」是作者自造的词——常规重构是业界标准概念,认知重构不是;它的价值在划清「为可读而改」和「为维护而改」的账,读者应当把它当工作方法接受,而不是当业界术语引用。

**书的坦白:**认知重构因人而异、多为临时性行为,所以它无法沉淀成团队规范;第 13 章会看到它的团队版变体。

8. 边界与局限

  • 改代码读懂了就回滚——认知重构的临时性是它的纪律,忘了回滚会把个人化的代码强加给团队(不同人的「好读」互相矛盾)。
  • 依赖图和状态表都吃体力:只对一段几百行的代码可行,整个代码库画不了;它们是「攻坚工具」,不是日常阅读姿势。
  • 内联是双刃剑:它减读的负荷,但如果那段代码随后要改,展开的重复又会加重维护——两种负荷又一次打架。
  • 本章的三种负荷只详讲了两种;第三种(关联认知负荷)与「学得会学不会」有关,第 11 章专门展开。

9. 可带走的

  1. 「读不动」先分诊:问题难(内部)躲不掉;写法差(外部)能减——大部分「烂代码读不懂」是外部负荷;
  2. 需要记笔记才能读的代码,先改写再读:内联一次调用的函数、把方法挪到调用旁、换掉看不懂的语法;
  3. 为读懂而改代码不丢人,但请在自己的分支上改,读懂就回滚;
  4. 读「结构难」画依赖图(圈变量→连线→圈调用→连声明),读「计算难」建状态表(一变量一列,一步一行);
  5. 状态表要填满,跳格就白画了;
  6. 「好读」没有绝对标准——对熟悉列表推导式的人它最短,对不熟悉的人 for 循环最短;
  7. 高级语法做抽认卡时,两面各写普通版和高级版,一次进货两件货。

10. 原文地图

主题原书章原文位置
工作记忆的定义4.1 为什么复杂的代码难以理解text/25-ch04-01-4-1.txt:24(搜「同义词」) · text/25-ch04-01-4-1.txt:26(搜「应用于某个问题的短时记忆」) · text/25-ch04-01-4-1.txt:28(搜「电话号码」)
认知负荷与过载4.1 为什么复杂的代码难以理解text/25-ch04-01-4-1.txt:34(搜「认知负荷(cognitive load)」)
三种负荷4.1 为什么复杂的代码难以理解text/25-ch04-01-4-1.txt:38(搜「John Sweller」) · text/25-ch04-01-4-1.txt:42(搜「关联认知负荷」)
内部负荷与固有复杂性4.1 为什么复杂的代码难以理解text/25-ch04-01-4-1.txt:50(搜「勾股定理」) · text/25-ch04-01-4-1.txt:50(搜「固有复杂性」)
外部负荷与偶发复杂性4.1 为什么复杂的代码难以理解text/25-ch04-01-4-1.txt:54(搜「调整一下问题的表述方式」) · text/25-ch04-01-4-1.txt:58(搜「偶发复杂性」)
外部负荷因人而异4.1 为什么复杂的代码难以理解text/25-ch04-01-4-1.txt:64(搜「above_ten」) · text/25-ch04-01-4-1.txt:69(搜「列表推导式的程序员」)
重构与认知重构4.2 减轻认知负荷的方法text/26-ch04-02-4-2.txt:9(搜「不改变代码的外部行为」) · text/26-ch04-02-4-2.txt:22(搜「上下滚动IDE屏幕」) · text/26-ch04-02-4-2.txt:24(搜「认知重构(cognitive refactoring)」)
反向重构与内联4.2 减轻认知负荷的方法text/26-ch04-02-4-2.txt:26(搜「反向重构」) · text/26-ch04-02-4-2.txt:30(搜「重新安排方法的顺序」)
理解分支4.2 减轻认知负荷的方法text/26-ch04-02-4-2.txt:34(搜「本地分支」)
替换 lambda 与列表推导式4.2 减轻认知负荷的方法text/26-ch04-02-4-2.txt:40(搜「lambda表达式」) · text/26-ch04-02-4-2.txt:56(搜「普通函数作为filter()方法的参数」) · text/26-ch04-02-4-2.txt:88(搜「for循环以方便理解」)
三元运算符4.2 减轻认知负荷的方法text/26-ch04-02-4-2.txt:99(搜「三元运算符」) · text/26-ch04-02-4-2.txt:111(搜「显著增加外部认知负荷」)
可读性因人而异4.2 减轻认知负荷的方法text/26-ch04-02-4-2.txt:113(搜「并不丢人」)
抽认卡两面写代码4.2 减轻认知负荷的方法text/26-ch04-02-4-2.txt:127(搜「两面都写上代码」)
依赖图六步4.3 利用记忆辅助工具解决工作记忆过载的问题text/27-ch04-03-4-3.txt:15(搜「依赖图(dependency graph)」) · text/27-ch04-03-4-3.txt:17(搜「圈出所有变量」) · text/27-ch04-03-4-3.txt:25(搜「用线连起来」)
只调用一次的方法、入口点4.3 利用记忆辅助工具解决工作记忆过载的问题text/27-ch04-03-4-3.txt:37(搜「只有一次调用的方法」) · text/27-ch04-03-4-3.txt:47(搜「入口点」)
状态表4.3 利用记忆辅助工具解决工作记忆过载的问题text/27-ch04-03-4-3.txt:53(搜「状态表(state table)」) · text/27-ch04-03-4-3.txt:55(搜「判断变量值」) · text/27-ch04-03-4-3.txt:89(搜「认知编译」)
Python Tutor4.3 利用记忆辅助工具解决工作记忆过载的问题text/27-ch04-03-4-3.txt:93(搜「Python Tutor」) · text/27-ch04-03-4-3.txt:97(搜「调试尤其有用」)
两工具分工4.3 利用记忆辅助工具解决工作记忆过载的问题text/27-ch04-03-4-3.txt:101(搜「组织方式」)

Footnotes

  1. 出处:「4.1 为什么复杂的代码难以理解」第 24 段(text/25-ch04-01-4-1.txt:24,搜「同义词」)与第 26 段(text/25-ch04-01-4-1.txt:26,搜「应用于某个问题的短时记忆」)。原文明确写道学界对二者是否同义有分歧、本书加以区分。

  2. 出处:「4.1 为什么复杂的代码难以理解」第 28 段(text/25-ch04-01-4-1.txt:28,搜「电话号码」)。

  3. 出处:「4.1 为什么复杂的代码难以理解」第 34 段(text/25-ch04-01-4-1.txt:34,搜「认知负荷(cognitive load)」)。

  4. 出处:「4.1 为什么复杂的代码难以理解」第 38 段(text/25-ch04-01-4-1.txt:38,搜「John Sweller」)。第三种负荷本章只点名,详见「10.4」第 33 段(text/62-ch10-04-10-4.txt:33,搜「关联认知负荷」)。

  5. 出处:「4.1 为什么复杂的代码难以理解」第 47、50 段(text/25-ch04-01-4-1.txt:50,搜「勾股定理」;text/25-ch04-01-4-1.txt:50,搜「固有复杂性」)。

  6. 出处:「4.1 为什么复杂的代码难以理解」第 54、58 段(text/25-ch04-01-4-1.txt:54,搜「调整一下问题的表述方式」;text/25-ch04-01-4-1.txt:58,搜「偶发复杂性」)。

  7. 出处:「4.1 为什么复杂的代码难以理解」第 69 段(text/25-ch04-01-4-1.txt:69,搜「列表推导式的程序员」)。

  8. 出处:「4.2 减轻认知负荷的方法」第 22 段(text/26-ch04-02-4-2.txt:22,搜「上下滚动IDE屏幕」)。

  9. 出处:「4.2 减轻认知负荷的方法」第 24 段(text/26-ch04-02-4-2.txt:24,搜「认知重构(cognitive refactoring)」)。

  10. 出处:「4.2 减轻认知负荷的方法」第 26 段(text/26-ch04-02-4-2.txt:26,搜「反向重构」)。原文举例的方法名是 calculate() 和 transform()。

  11. 出处:「4.2 减轻认知负荷的方法」第 30 段(text/26-ch04-02-4-2.txt:30,搜「重新安排方法的顺序」)。

  12. 出处:「4.2 减轻认知负荷的方法」第 40、56、88 段(text/26-ch04-02-4-2.txt:40,搜「lambda表达式」;text/26-ch04-02-4-2.txt:56,搜「普通函数作为filter()方法的参数」;text/26-ch04-02-4-2.txt:88,搜「for循环以方便理解」);三元运算符见第 99 段(text/26-ch04-02-4-2.txt:99,搜「三元运算符」)。

  13. 出处:「4.2 减轻认知负荷的方法」第 113 段(text/26-ch04-02-4-2.txt:113,搜「并不丢人」)。

  14. 出处:「4.2 减轻认知负荷的方法」第 34 段(text/26-ch04-02-4-2.txt:34,搜「本地分支」)。

  15. 出处:「4.2 减轻认知负荷的方法」第 127 段(text/26-ch04-02-4-2.txt:127,搜「两面都写上代码」)。

  16. 出处:「4.3 利用记忆辅助工具解决工作记忆过载的问题」第 15–45 段(text/27-ch04-03-4-3.txt:15,搜「依赖图(dependency graph)」)。六步分别见第 17、23、31、35、39、43 段。

  17. 出处:「4.3 利用记忆辅助工具解决工作记忆过载的问题」第 47 段(text/27-ch04-03-4-3.txt:47,搜「入口点」)。

  18. 出处:「4.3 利用记忆辅助工具解决工作记忆过载的问题」第 53、55 段(text/27-ch04-03-4-3.txt:53,搜「状态表(state table)」;text/27-ch04-03-4-3.txt:55,搜「判断变量值」)。

  19. 出处:「4.3 利用记忆辅助工具解决工作记忆过载的问题」第 89 段(text/27-ch04-03-4-3.txt:89,搜「最好不要这样做」)。原文告诫不要只填部分变量。

  20. 出处:「4.3 利用记忆辅助工具解决工作记忆过载的问题」第 93、97 段(text/27-ch04-03-4-3.txt:93,搜「Python Tutor」;text/27-ch04-03-4-3.txt:97,搜「调试尤其有用」)。作者为加州大学圣迭戈分校郭伽。

  21. 出处:「4.3 利用记忆辅助工具解决工作记忆过载的问题」第 101 段(text/27-ch04-03-4-3.txt:101,搜「组织方式」)。