26 如何保持资源的平衡

让获取资源的一方承担释放责任,以作用域、所有权和异常路径证明资源始终平衡。

学习目标

  • 能解释资源为什么必须由获取者负责释放,并区分正常、提前返回与异常路径
  • 能修改一段资源处理代码,把清理动作放进拥有资源的最小作用域并保留回退证据
  • 能回答:如果异常发生在移交之后,哪一个节点首先失去平衡,怎样证明没有泄漏?

从一把借来的伞开始

你从前台拿走一把伞,走到门口才发现开始下雨。无论你顺利走到家、半路折返,还是把伞交给同事,都必须有人知道现在由谁保管,最后把伞放回指定位置。

如果只记录“拿走了”,却没有记录“谁归还、何时归还”,晴天的演练看起来没有问题;一场突然的暴雨就会把责任藏在半路。软件里的文件、锁、连接和临时目录也一样:真正难的不是拿到它,而是让每条出口都回到平衡。

这章要守住哪条边界

本页依据 David Thomas、Andrew Hunt《程序员修炼之道:通向务实的最高境界(第2版)》,云风译,电子工业出版社,2020年4月,ISBN 9787121384356 的公开完整中文目录,独立重构 26 如何保持资源的平衡提示40:有始有终提示41:在局部行动。示例、代码、图示、实验和练习均为本课程重新设计,不复制原书正文、插图、练习答案或代码。

资源平衡不是“最后看起来没报错”,而是能沿着一条具体路径回答三件事:谁拥有资源、谁在什么范围内使用、谁负责在每个出口清理。下面的模型把这条责任链拆成五个可观察节点:获取 → 使用 → 移交 → 异常 → 释放。

先看责任如何沿链移动

先预测:如果在“使用”之后立刻抛出异常,清理责任应该停在哪里?再点击实验台的“下一步”,观察每个节点留下的证据;最后打开故障开关,找出第一个不再可信的节点。

Topic 26 · 资源平衡实验台
资源平衡的获取到释放链26 如何保持资源的平衡:责任随资源一起移动获取一次,就必须在正常、提前返回和异常路径中释放一次1获取证据:记录所有者复核:资源所有权责任仍在手上2使用证据:限制作用域复核:作用域等待前置证据3移交证据:交接清晰复核:异常安全等待前置证据4异常证据:沿反向路径复核:泄漏等待前置证据5释放证据:恰好一次复核:局部性等待前置证据验收不变量:获取次数 = 释放次数把所有权、作用域、异常安全和回退动作写在同一段代码附近提示40:有始有终 · 提示41:在局部行动

第 1 / 5 步:获取 已留下可复核证据。

先预测首差,再推进链路;重置后应回到获取节点和未注入状态。
分步1 / 3

1. 获取:先写清谁拥有

资源平衡的获取到释放链26 如何保持资源的平衡:责任随资源一起移动获取一次,就必须在正常、提前返回和异常路径中释放一次1获取证据:记录所有者复核:资源所有权责任仍在手上2使用证据:限制作用域复核:作用域等待前置证据3移交证据:交接清晰复核:异常安全等待前置证据4异常证据:沿反向路径复核:泄漏等待前置证据5释放证据:恰好一次复核:局部性等待前置证据验收不变量:获取次数 = 释放次数把所有权、作用域、异常安全和回退动作写在同一段代码附近提示40:有始有终 · 提示41:在局部行动
每个节点都留下责任证据;异常只改变释放路径,其他条件保持不变。

资源一旦被获取,就要同时写下所有者、初始状态和释放承诺;没有这三项,后面的“清理”无法验收。

常见误区与回退

五个节点怎样形成资源合同

获取:记录资源所有权

先把 <Term def="表示当前由哪个作用域或对象负责资源的使用与释放;责任不能同时悬空或重复。">资源所有权</Term>写在获取点旁边。打开文件的函数、建立连接的服务或加锁的临界区,必须能回答“谁拿到它、谁有权释放它”。所有权不是变量名,而是一个可交接的责任合同。

使用:缩小作用域

<Term def="资源从获取到释放之间允许被访问的最小时间与代码范围。">作用域</Term>越小,越容易枚举出口。把文件句柄跨过多个服务层传递,会让每一层都可能误以为自己负责;在同一个函数或明确的资源对象内完成读取、写入和清理,反而更容易复核。

移交:把异常安全写进边界

当资源必须交给下游时,要交接 <Term def="无论正常返回、提前返回还是抛出异常,资源处理都不会破坏所有权合同的性质。">异常安全</Term>。移交前写清“交给谁、从哪一刻起旧持有者不再操作、失败时退回谁”。如果这些问题只能靠调用顺序猜,就还没有完成真正的移交。

异常:先找出泄漏的首差

<Term def="已经获取的资源没有在责任路径结束时被释放,导致它继续占用系统能力。">泄漏</Term>不是“内存变大”这一种现象,未关闭的文件、未归还的锁和未断开的连接都算。故障实验应只移除一条清理路径,记录首个分叉和未覆盖输入;不要用最后的成功响应替代现场。

释放:让局部性帮助复核

<Term def="把数据与资源处理放在相近、边界清楚的代码位置,减少跨层传递与隐藏副作用。">局部性</Term>让释放动作靠近获取动作,也让审查者可以从一段代码中看到完整生命周期。局部性不是拒绝抽象,而是要求抽象携带清晰的拥有者、回退动作与关闭语义。

贯穿全链:维护资源不变量

最后定义 <Term def="在每条允许的执行路径上都必须成立的资源事实,例如获取次数与释放次数相等。">资源不变量</Term>:获取一次就释放一次,移交之后只有新所有者能释放,任何异常都不能跳过清理。这个不变量比“请求返回 200”更接近真实验收标准。

提示40:有始有终

“有始有终”不是把 close() 机械地贴到函数末尾,而是从获取的第一行就声明释放责任。打开文件后立刻进入一个最小保护范围;每次提前返回都经过同一个清理出口;释放失败则记录上下文并决定回退,而不是静默吞掉。

下面的 TypeScript 片段把资源的获取、使用和释放放在同一个生命周期中。代码练习会要求你改动它,重点不是语法,而是让读者能在眼前枚举三条路径。

async function readManifest(path: string) {
  const file = await openFile(path);
  try {
    const text = await file.readText();
    if (!text.trim()) return { kind: "empty" };
    return { kind: "manifest", text };
  } finally {
    await file.close();
  }
}

finally 只是实现手段;真正的合同是“获取者承担释放责任”。如果 close() 本身可能失败,也要规定它如何记录、重试或交给隔离流程,不能因为主工作成功就假设资源已经回收。

提示41:在局部行动

“在局部行动”强调的是责任半径,而不是把所有代码塞进一个超长函数。资源应该在最接近获取点的对象或作用域内使用;需要向外传递时,传递一个明确的能力或结果,而不是把未说明生命周期的裸句柄散落到各层。

局部行动也保护异常路径:当一个操作失败,最近的拥有者最先知道哪些副作用已经发生、哪些还没有发生。它可以停止、释放并提供重放输入;远处的调用者只看到一个模糊错误时,已经太晚了。

用一个可复核的清单验收

每次改资源代码,都保存下面这组最小证据:

检查点必答问题不通过时的动作
获取谁得到资源,初始状态是什么?拒绝继续,先补所有权记录
使用哪个最小作用域可以访问它?缩短作用域,删除隐式共享
移交哪一刻责任从谁转给谁?明确交接,撤销旧持有者权限
异常每个提前出口如何清理?注入单故障并保存首差
释放是否恰好一次,失败如何回退?隔离资源,重放原始输入

当审查者只拿到输入、验收命题和边界条件时,也应该能够重建“正常、提前返回、异常”三条路径。若只能依赖作者记忆点击实验台,就还没有形成可移交的工程证据。

本章回顾

  • 获取者同时承担资源所有权与释放责任。
  • 最小作用域降低泄漏、重复释放和隐藏移交的风险。
  • 正常、提前返回、异常三条路径都要闭合。
  • 发现首差后保留上下文,停止副作用,再从原始输入回退。
  • 提示40:有始有终提示41:在局部行动共同要求责任可见、边界局部。

练习

练习

  1. 路径审查:围绕“提示40:有始有终”,为一个打开文件的函数画出正常返回、空输入提前返回和读取异常三条路径,并在每条路径标出释放动作。
  1. 场景判断:围绕“提示41:在局部行动”,一个服务需要把数据库连接传给三个下游函数。你会传递裸连接、封装后的短操作,还是让下游接管所有权?说明选择和拒绝条件。
  1. 改 Demo 代码:把本章的 readManifest 改成能注入一次读取失败的版本,要求仍然释放文件、报告首差,并在实验台中用“重置实验台”重放同一输入,不能把失败改成空结果。

名词解释

名词解释

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

资源所有权

谁负责使用并最终释放资源的明确约定;责任不能悬空,也不能由两个人重复承担。

作用域

资源从拿到手到交还之间允许被访问的最小时间和代码范围。

异常安全

即使中途失败或提前离开,资源责任仍然能按合同完成交接或释放。

泄漏

资源已经拿到,却没有在生命周期结束时交还,导致它继续占用系统能力。

局部性

把相关数据、操作和清理放在相近且边界清楚的位置,减少隐藏的跨层影响。

资源不变量

每条允许路径都必须成立的资源事实,例如获取一次就释放一次。

来源与改写范围

本页是基于公开目录的 independent rewrite;资源生命周期模型、TypeScript 示例、图示、实验和练习均为本课程重新设计,不声称提供原书全文或原书答案。

前后导航

讨论

评论区加载中…