深入 Ruby 06:block、Proc 与 lambda
Ruby 的 block 可以让遍历方法把每项任务交给调用者处理。保存这段代码时常用 Proc,要求严格参数数量时可以用 lambda。它们并不是三个可随意替换的名称;执行位置、参数处理和退出行为都需要保持清楚。
文章卡
| 项目 | 内容 |
|---|---|
| 先修 | 第 05 篇的位置、关键字与 block 通道 |
| 实验 | bundle exec ruby labs/06/run.rb |
| 核心问题 | 谁调用 block,Proc 怎样接收参数,lambda 严格到哪里? |
| 验收 | yield 轨迹、无 block 返回枚举器、数量错误与数组解构 |
| 边界 | 参数行为在本篇;非局部 return 与 break 在第 07 篇 |
yield 调用当前方法收到的 block
实验方法把两个值交给调用者,并且有一个与 block 结果无关的最终返回值:
1 | |
轨迹证明 block 被调用两次,参数依次是 3 与 4。最终结果 :finished 来自方法最后的表达式,而不是自动收集的 block 结果。map 之所以收集结果,是它的实现与契约如此,不能外推为所有带 block 的方法都这样工作。
yield 不需要先把 block 显式保存在局部变量里。需要把它传给另一个方法或长期保存时,再用 &block 捕获。是否立即执行、执行次数和是否存储,都是 API 契约;只写“接受一个 block”还不足以让调用者判断副作用。
没有 block 时直接 yield 会失败。本篇选择返回 Enumerator,使同一方法既能进行内部迭代,也能被调用者拉取。twice(3).to_a 得到 [3, 4],第 08 篇再把这一惯例与 Enumerable 接口组合。
Proc 是对象,block 是调用的专用通道
proc { ... } 得到可以保存并调用的 Proc 对象。方法可以把这个对象作为普通位置参数接收,也可以用 & 将它放进 block 通道。这两个调用形状不同,不能仅因内容都是代码就认为自动互换。
1 | |
前一条通过 call 调用对象,后一条把对象转换成 map 收到的 block。&object 需要相应转换能力,不能把所有支持 call 的对象都默认当成 Proc。方法可以只约定调用 call,这样普通对象也能满足协议,而不必继承 Proc。
本篇 lab 创建了一个 Object,为它定义 call(value),结果同样是 8。这个实验只证明调用协议成立,不说明该对象可以通过 & 转换。第 14 篇会区分“支持一个方法”和“支持某个隐式转换协议”。
非 lambda Proc 可以宽松绑定位置参数
lab 用同样的两个参数声明比较两种 Proc:
1 | |
缺少的第二个位置参数得到 nil,多出的第三个参数不参与绑定,单个数组实参被解开为两个值。这种行为适合某些迭代协议,却也会掩盖回调签名错误。lambda 的对应调用会对错误数量抛 ArgumentError。Proc 参数规则
这里的“宽松”有范围:不要推导为关键字要求、类型要求和业务约束都被取消。一个回调签名里包含必须关键字,仍应对实际错误调用单独测试;函数体中的数值操作也不会因为接收者是 Proc 就允许 nil。
lambda 也是 Proc 对象,lambda? 可以查询它的语义。经过 & 传递已有 Proc 时,还需要注意语义的保留;不能假定方法名写着 lambda 就能把任意非 lambda Proc 改造为严格回调。第 16 篇中的 define_method 又有方法定义自己的规则,需要独立验收。
严格数量与显式解构可以同时存在
strict.call([1, 2]) 传了一个位置参数,不满足两个位置参数的数量。本篇捕获这个 ArgumentError,并让未发生错误的情况失败。需要接收一个数组后再拆开,可以把拆解写进形参:
1 | |
这个 lambda 的调用数量是一项;这一项内部再进行解构。它说明“lambda 不支持解构”是错误结论,准确区别是自动宽松处理与显式形参结构。
Taskbook 的任务是 Hash,不需要为了演示解构把每项转换成位置数组。字段名比固定位置更能表达用途。只有协议天然产生元组时,再考虑解构;若外部结构有缺失和额外值,还需进行领域检查。
Symbol 转 block 是一项协议
%w[ruby block].map(&:length) 得到 [4, 5]。这里 :length 通过 to_proc 转换成可用 block,再对各元素发送对应消息。它适合简短的无参数转换;若处理需要多项条件、错误分类或副作用,显式 block 通常更清楚。
不要把 &:name 解释成字符串替换,也不要借它掩盖动态方法名来源。对用户提供的方法名调用 public_send 或 send,仍需要允许集合;能表达动态调用不等于应该对外开放全部对象能力。
map 的 block 接收和结果收集是本篇的局部使用,资源 block 则常用于保证关闭,DSL block 可能改变 self。相同的外观下,调用方、返回值和环境契约可能不同。后续章节会逐个实验,而不是把 block 统一当成一种“语法糖”。
实验与接口选择
运行 labs/06/run.rb 输出 yield=[3, 4]、proc=[1, 2] 和 lambda_destructure=[1, 2],最终为 PASS 06。数量错误与无 block 枚举两条路径都有判定。
即时遍历可以接受 block;需要保存回调可以接受支持 call 的对象;需要严格调用形状可以选 lambda 并继续检查返回值。API 文档应写明回调何时执行、传入几项、期望什么结果,以及错误如何传播。第 07 篇会增加退出目标这一项约定。
练习是给 twice 增加一个结果收集版本,返回两次 block 结果构成的数组,检查它与原方法的返回值区别。另一个练习是把两个参数的 lambda 交给产生一个数组的调用者,先保留数量失败,再改为显式解构;两条路径都要能够被断言识别。
