3.7 我是一个函数
从逻辑散落在可跳转语句中走到命名调用边界压缩并复用推理,沿函数参数、栈帧、返回和异常边界诊断隐式状态。
学习目标
- 能沿绑定参数、建立栈帧、执行函数体、返回或抛错和释放栈帧追踪一次函数调用
- 能解释参数、局部变量、返回值、异常和调用栈各自的生命周期与边界
- 能在正常、边界和故障场景中回答:同样输入却因隐式全局状态得到不同结果时,哪一个调用合同已被破坏
为什么需要这一机制
本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 3.7 我是一个函数。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。
3.7 我是一个函数 不能停在“把代码取个名字”。真实系统要解决的变化是从 逻辑散落在可跳转语句中 到 命名调用边界压缩并复用推理;可执行机制是:函数把参数、局部状态、返回值与异常封装成调用合同,调用栈保存活动记录,纯函数还能用相同输入重放结果。
三个会让函数调用失真的陷阱
一个目录节点到函数调用证据
3.7 我是一个函数
在 ↡用名称、参数、局部状态、返回值和异常把一段可调用行为封装成可推理合同的程序边界。 中,函数不是跳转标签,而是一个有生命周期的活动记录。合同可以写成
其中输入、输出、前置条件、后置条件和异常都必须可观察;如果函数偷偷读取全局变量,公式里的输入就不完整。
绑定参数
↡把实参按调用规则对应到形参,并在调用开始时固定参数值或引用关系的动作。决定函数本次调用看到的输入。要记录参数名、值、类型和传递方式;改变一个参数后,才能判断后续差异来自绑定还是函数体。
建立栈帧
↡为一次活动调用创建参数、局部变量、返回地址和父调用关系等运行时记录的过程。把“同一个函数”与“这一次调用”分开。递归和嵌套调用各有独立 frame ID,异常返回也不能跳过清理。
执行函数体
↡在当前栈帧中按语句和控制流计算局部状态,可能产生返回值或异常的阶段。应只读写合同声明的输入与状态。纯函数可以用相同输入重放;有副作用的函数必须把外部变化列入证据。
返回或抛错
↡函数完成后交付满足后置条件的返回值,或在前置条件失败时交付可诊断异常的结果边界。是调用方判断成功与拒绝的位置。不要把异常变成无意义的默认值,也不要在结果已交付后继续修改调用者可见状态。
释放栈帧
↡调用结束后清理本次参数、局部变量和返回地址记录,使活动调用数回到正确状态的过程。必须在正常返回和异常路径都发生。复位实验时,栈深度、frame ID 和全局状态都应恢复到基线。
最小可重放实现
const input = fixedCase();
const expected = referenceModel(input);
const actual = runProgram(input);
assert.deepEqual(actual, expected);
assert.deepEqual(resetAndRun(input), expected);这段实现草图只表达函数调用合同,不复制原书叙事或代码。实际运行时应保存输入、参数绑定、frame ID、局部状态、返回或异常、释放事件和复位结果,使另一位读者能够从干净状态重放。
五步复核一次函数调用
1. 绑定参数并固定前置条件
记录函数名、形参、实参、类型和前置条件。先预测正常输入的返回值与副作用,再只改变一个参数,确认差异从绑定边界开始。
Lab
函数调用合同与隐藏状态实验
先预测参数、返回值和栈深度,再切换正常、边界与隐藏状态样本并重放。
相同输入得到相同返回值,栈深度正常回落
input=4 → frame=f-01 → result=16 → frame=released → replay=match
判定
accept:输入、结果与生命周期闭环
当前样本:纯函数基线;保存参数、frame ID、栈深度、全局快照、返回或异常和复位轨迹。
正常、边界与故障证据矩阵
| 样本 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 参数满足前置条件,函数无隐藏状态 | 返回值与重放轨迹一致 | 参数绑定、frame ID、后置条件 |
| 边界 | 参数接近类型、范围或递归深度边界 | 明确返回或可诊断拒绝 | 类型/范围检查、栈深度、策略 |
| 故障 | 函数读取或修改未声明的全局状态 | 拒绝纯函数声明并定位首个偏离 | 全局快照、调用次序、异常和复位轨迹 |
故障诊断:先分开输入、栈帧与隐藏状态
- 绑定侧:查形参与实参的名称、类型、值和传递方式;不要把参数错配归因给函数体。
- 栈帧侧:查 frame ID、父调用、返回地址、局部变量和栈深度;递归调用必须逐层对应。
- 函数体侧:查控制流、局部状态、外部读取和副作用;纯函数声明与实际访问必须一致。
- 结果侧:查后置条件、异常类型、释放事件和调用方可见状态;默认值不能掩盖拒绝。
如果同一输入的结果随调用次序变化,先比较全局状态快照,再检查参数绑定;如果异常后栈深度不回落,查异常清理路径;如果返回值正确但后续请求错误,查函数体是否留下了未声明副作用。每次只改变一个边界,并重放可信基线。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 3.7 我是一个函数
用参数、局部状态、返回值和异常把可调用行为封装成可推理合同的程序边界。
- 绑定参数
将实参按调用规则对应到形参,并固定本次调用的输入关系。
- 建立栈帧
为一次调用创建参数、局部变量、返回地址和父调用关系的运行时记录。
- 执行函数体
在当前栈帧中按控制流计算局部状态并产生结果或异常的阶段。
- 返回或抛错
交付满足后置条件的返回值,或交付可诊断异常的结果边界。
- 释放栈帧
调用结束后清理本次活动记录并恢复正确栈深度的过程。
练习
练习
问题 1(3.7 我是一个函数): 为什么函数签名只有参数和返回类型还不够成为完整调用合同?
问题 2(绑定参数、建立栈帧、执行函数体): 递归调用同一个函数时,为什么每一层都需要独立活动记录?
问题 3(返回或抛错、释放栈帧): 设计一次非法输入实验,证明吞掉异常并返回默认值会如何破坏调用方的判断。
本页小结
3.7 我是一个函数 的关键不是给一段代码贴上名称,而是能解释参数绑定、栈帧生命周期、函数体状态、返回/抛错和释放之间的调用合同。完成标准是从固定输入出发,定位隐式全局状态造成的首个偏离,并证明正常与异常路径都能复位后重新得到基线轨迹。