深入 Ruby 07:闭包与非局部控制流
一段带 return 的 block 在方法内部调用时可以正常返回,保存后等方法结束再调用,却可能抛 LocalJumpError。原因不是代码内容改变,而是退出目标已经不在当前执行路径里。闭包同时保存环境,控制语句还携带特定的退出语义,两者必须分开分析。
文章卡
| 项目 | 内容 |
|---|---|
| 先修 | 第 06 篇的 Proc、lambda 和 yield |
| 实验 | bundle exec ruby labs/07/run.rb |
| 核心问题 | return、break、next 分别终止哪次执行? |
| 验收 | 有效与失效的 return/break、lambda 局部返回、共享闭包绑定 |
| 工程边界 | 长期保存回调优先使用明确的 call 返回契约 |
闭包捕获绑定,不是输入快照
两个闭包可以访问同一个外层局部变量:
1 | |
observe 看到的是已经修改后的绑定。闭包不是把创建时的 0 自动复制到一个私有字段里。捕获的变量还可以指向一个可变对象,其他引用修改它时,闭包也可能看到变化。
这项能力适合局部计数和配置工厂,也会延长被捕获对象的可达时间。把大型任务数组捕获进长期缓存回调,可能令数组无法回收;第 34 篇将可达性与分配实验连接起来。本篇不通过一个计数器就宣称闭包没有内存成本或线程风险。
保存回调时,必须说明它读取实时配置还是创建时配置。若需要快照,显式复制相应结构;若需要共享状态,则建立更新与同步契约。第 30 篇会验证跨 Thread 访问,单线程闭包计数并不能证明并发正确。
非 lambda Proc 的 return 指向定义方法
实验方法在自己的调用仍有效时执行 Proc:
1 | |
Proc 中的 return 结束 live_return,最后一行不执行。这不是 callback.call 返回一个值后方法自然继续,而是一次非局部控制转移。Proc 控制流
将回调从即时执行改成队列存储,不只改变时间,还可能改变目标是否有效。定义方法返回以后,该 Proc 再执行 return,不能重新返回一个已经结束的方法调用。
1 | |
这里的闭包仍存在,捕获的环境并没有因此消失;失效的是 return 的退出目标。捕获局部变量与恢复已结束调用是不同能力。错误原因记录为 :return,使反例能检查具体语义,而不是碰到任意异常都通过。
lambda 的 return 结束自身调用
同样把代码放进 lambda:
1 | |
lambda 返回后,外层方法继续执行,第二个值出现在结果里。长期存储的转换函数通常需要这样的局部返回语义,因为调用时不应突然退出定义它的业务方法。
但“使用 lambda 就完全安全”也不成立。lambda 仍可捕获共享数据、抛异常、执行 I/O 和产生副作用。它解决的是特定参数与返回规则,不能代替资源管理、输入校验和并发协议。
如果只是产生最终值,不一定需要显式 return。让最后表达式产生回调结果,可以减少退出目标的歧义;需要提前结束时,再明确选择 lambda 或普通方法中的 return。代码形式与公开契约要一致。
break 结束接收 block 的调用
本篇使用一个简单方法:
1 | |
break 使 invoke 这次调用结束,并把给定值作为调用结果;方法末尾未执行。next 结束本次 block 执行,将值返回给 yield,外层方法随后继续,最终得到 :after_yield。两个语句的位置相同,目标却不同。
遍历回调中使用 next,可以略过当前项的处理;break 可以提早结束遍历。把这段 block 改成独立 Proc 后,不能默认仍存在同一个接收调用目标。第 08 篇中 Lazy 的终止是枚举协议自己的需求控制,也不等于任意回调中的 break 都可以被延后执行。
保存 break 也可能保存失效目标
实验方法将收到的 block 返回,而不当场调用:
1 | |
escaped_break 的那次调用已经结束,later call 不能再结束它。lab 断言 LocalJumpError 的原因是 :break,与前面的 :return 分开。只有测试“方法返回后执行”的路径,才会暴露这个问题;在定义方法内立即调用并通过,不能证明保存后的语义。
API 因此应明确即时回调与长期回调。配置 block 若在构造阶段立刻求值,可以明确限定控制范围。保存事件回调、任务队列或重试函数时,应约定普通 call 结果与异常传播,避免依赖已经退出的栈帧。
退出仍需要经过资源清理
本篇将正常返回和局部错误分开验证。第 18 篇的 ensure 将说明退出过程中的清理顺序,包括 return 与异常路径;不要为了保住清理而在 ensure 中再写 return,它可能覆盖原有结果或异常。
回调抛出 LocalJumpError 时,调用者通常应让这个编程错误被诊断出来。把所有错误捕获后返回空数组,会使 Taskbook 像是没有符合条件的任务,真正的回调错误却消失。错误分类是数据管道契约的一部分。
实验与练习
labs/07/run.rb 实际输出有效 return 的结果、lambda 外层继续结果、break 与 next 的目标,以及 closure=2,最后 PASS 07。两个失效目标的反例都有类型和 reason 检查。
练习是写一个保存回调的容器:先放入带 return 的非 lambda Proc,在创建方法结束后调用并断言失败;随后改成 lambda,检查正常结果。另一个练习是用两个闭包共享配置 Hash,分别实现实时读取和显式快照,修改外部配置后比较返回值。不要通过猜测“闭包应该复制”决定结果。
参考资料
系列导航
导读 · 上一篇:06:block、Proc 与 lambda · 下一篇:08:Enumerable、Enumerator 与 Lazy · 完整源码包

