3.7 我是一个函数

从逻辑散落在可跳转语句中走到命名调用边界压缩并复用推理,沿函数参数、栈帧、返回和异常边界诊断隐式状态。

学习目标

  • 能沿绑定参数、建立栈帧、执行函数体、返回或抛错和释放栈帧追踪一次函数调用
  • 能解释参数、局部变量、返回值、异常和调用栈各自的生命周期与边界
  • 能在正常、边界和故障场景中回答:同样输入却因隐式全局状态得到不同结果时,哪一个调用合同已被破坏

为什么需要这一机制

本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 3.7 我是一个函数。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。

3.7 我是一个函数 不能停在“把代码取个名字”。真实系统要解决的变化是从 逻辑散落在可跳转语句中命名调用边界压缩并复用推理;可执行机制是:函数把参数、局部状态、返回值与异常封装成调用合同,调用栈保存活动记录,纯函数还能用相同输入重放结果。

函数调用链:输入、活动记录与结果必须成对出现同一输入的可重放性,取决于合同是否包含所有真实依赖1绑定参数实参 → 形参输入证据2建立栈帧frame + locals活动记录边界3执行函数体状态 + 控制流结果证据4返回或抛错结果 + 异常结果证据5释放栈帧深度回落结果证据正常返回和异常返回都必须释放栈帧,不能留下未声明状态
专属图示:把 3.7 我是一个函数 的调用边界拆成可重放的生命周期。

三个会让函数调用失真的陷阱

一个目录节点到函数调用证据

3.7 我是一个函数

中,函数不是跳转标签,而是一个有生命周期的活动记录。合同可以写成

y=f(x)with declared pre/post conditionsy=f(x)\quad\text{with declared pre/post conditions}

其中输入、输出、前置条件、后置条件和异常都必须可观察;如果函数偷偷读取全局变量,公式里的输入就不完整。

绑定参数

决定函数本次调用看到的输入。要记录参数名、值、类型和传递方式;改变一个参数后,才能判断后续差异来自绑定还是函数体。

建立栈帧

把“同一个函数”与“这一次调用”分开。递归和嵌套调用各有独立 frame ID,异常返回也不能跳过清理。

执行函数体

应只读写合同声明的输入与状态。纯函数可以用相同输入重放;有副作用的函数必须把外部变化列入证据。

返回或抛错

是调用方判断成功与拒绝的位置。不要把异常变成无意义的默认值,也不要在结果已交付后继续修改调用者可见状态。

释放栈帧

必须在正常返回和异常路径都发生。复位实验时,栈深度、frame ID 和全局状态都应恢复到基线。

函数调用链:输入、活动记录与结果必须成对出现同一输入的可重放性,取决于合同是否包含所有真实依赖1绑定参数实参 → 形参输入证据2建立栈帧frame + locals活动记录边界3执行函数体状态 + 控制流结果证据4返回或抛错结果 + 异常结果证据5释放栈帧深度回落结果证据正常返回和异常返回都必须释放栈帧,不能留下未声明状态
专属图示:把 3.7 我是一个函数 的调用边界拆成可重放的生命周期。

最小可重放实现

const input = fixedCase();
const expected = referenceModel(input);
const actual = runProgram(input);
assert.deepEqual(actual, expected);
assert.deepEqual(resetAndRun(input), expected);

这段实现草图只表达函数调用合同,不复制原书叙事或代码。实际运行时应保存输入、参数绑定、frame ID、局部状态、返回或异常、释放事件和复位结果,使另一位读者能够从干净状态重放。

五步复核一次函数调用

分步1 / 5

1. 绑定参数并固定前置条件

记录函数名、形参、实参、类型和前置条件。先预测正常输入的返回值与副作用,再只改变一个参数,确认差异从绑定边界开始。

函数调用链:输入、活动记录与结果必须成对出现同一输入的可重放性,取决于合同是否包含所有真实依赖1绑定参数实参 → 形参输入证据2建立栈帧frame + locals活动记录边界3执行函数体状态 + 控制流结果证据4返回或抛错结果 + 异常结果证据5释放栈帧深度回落结果证据正常返回和异常返回都必须释放栈帧,不能留下未声明状态
专属图示:把 3.7 我是一个函数 的调用边界拆成可重放的生命周期。

Lab

函数调用合同与隐藏状态实验

先预测参数、返回值和栈深度,再切换正常、边界与隐藏状态样本并重放。

相同输入得到相同返回值,栈深度正常回落

input=4 → frame=f-01 → result=16 → frame=released → replay=match

判定

accept:输入、结果与生命周期闭环

当前样本:纯函数基线;保存参数、frame ID、栈深度、全局快照、返回或异常和复位轨迹。

正常、边界与故障证据矩阵

函数合同证据矩阵:结果正确不代表生命周期正确正常样本看闭环,边界样本看拒绝,故障样本看隐藏状态与栈清理观察项正常边界故障输入参数满足类型边界隐藏读取栈帧深度匹配递归加深未释放结果可重放明确拒绝默认值状态无副作用依赖声明全局变更先记录首个偏离,再判断返回值是否满足完整合同
专属图示:把参数、栈帧、结果和副作用放进同一份验收矩阵。
样本只改变的变量预期判定必存证据
正常参数满足前置条件,函数无隐藏状态返回值与重放轨迹一致参数绑定、frame ID、后置条件
边界参数接近类型、范围或递归深度边界明确返回或可诊断拒绝类型/范围检查、栈深度、策略
故障函数读取或修改未声明的全局状态拒绝纯函数声明并定位首个偏离全局快照、调用次序、异常和复位轨迹

故障诊断:先分开输入、栈帧与隐藏状态

  1. 绑定侧:查形参与实参的名称、类型、值和传递方式;不要把参数错配归因给函数体。
  2. 栈帧侧:查 frame ID、父调用、返回地址、局部变量和栈深度;递归调用必须逐层对应。
  3. 函数体侧:查控制流、局部状态、外部读取和副作用;纯函数声明与实际访问必须一致。
  4. 结果侧:查后置条件、异常类型、释放事件和调用方可见状态;默认值不能掩盖拒绝。

如果同一输入的结果随调用次序变化,先比较全局状态快照,再检查参数绑定;如果异常后栈深度不回落,查异常清理路径;如果返回值正确但后续请求错误,查函数体是否留下了未声明副作用。每次只改变一个边界,并重放可信基线。

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

3.7 我是一个函数

用参数、局部状态、返回值和异常把可调用行为封装成可推理合同的程序边界。

绑定参数

将实参按调用规则对应到形参,并固定本次调用的输入关系。

建立栈帧

为一次调用创建参数、局部变量、返回地址和父调用关系的运行时记录。

执行函数体

在当前栈帧中按控制流计算局部状态并产生结果或异常的阶段。

返回或抛错

交付满足后置条件的返回值,或交付可诊断异常的结果边界。

释放栈帧

调用结束后清理本次活动记录并恢复正确栈深度的过程。

练习

练习

问题 1(3.7 我是一个函数): 为什么函数签名只有参数和返回类型还不够成为完整调用合同?

问题 2(绑定参数、建立栈帧、执行函数体): 递归调用同一个函数时,为什么每一层都需要独立活动记录?

问题 3(返回或抛错、释放栈帧): 设计一次非法输入实验,证明吞掉异常并返回默认值会如何破坏调用方的判断。

资料与写作方式声明

本章以码农翻身权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

本页小结

3.7 我是一个函数 的关键不是给一段代码贴上名称,而是能解释参数绑定、栈帧生命周期、函数体状态、返回/抛错和释放之间的调用合同。完成标准是从固定输入出发,定位隐式全局状态造成的首个偏离,并证明正常与异常路径都能复位后重新得到基线轨迹。

讨论

评论区加载中…