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
2
3
4
5
6
7
8
9
10
11
def twice(value)
return enum_for(__method__, value) unless block_given?
yield value
yield value + 1
:finished
end

events = []
result = twice(3) { |item| events << item }
raise unless result == :finished
raise unless events == [3, 4]

轨迹证明 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
2
3
callback = proc { |value| value * 2 }
raise unless callback.call(4) == 8
raise unless [1, 2].map(&callback) == [2, 4]

前一条通过 call 调用对象,后一条把对象转换成 map 收到的 block。&object 需要相应转换能力,不能把所有支持 call 的对象都默认当成 Proc。方法可以只约定调用 call,这样普通对象也能满足协议,而不必继承 Proc。

本篇 lab 创建了一个 Object,为它定义 call(value),结果同样是 8。这个实验只证明调用协议成立,不说明该对象可以通过 & 转换。第 14 篇会区分“支持一个方法”和“支持某个隐式转换协议”。

非 lambda Proc 可以宽松绑定位置参数

lab 用同样的两个参数声明比较两种 Proc:

1
2
3
4
5
6
loose = proc { |a, b| [a, b] }
strict = ->(a, b) { [a, b] }

raise unless loose.call(1) == [1, nil]
raise unless loose.call(1, 2, 3) == [1, 2]
raise unless loose.call([1, 2]) == [1, 2]

缺少的第二个位置参数得到 nil,多出的第三个参数不参与绑定,单个数组实参被解开为两个值。这种行为适合某些迭代协议,却也会掩盖回调签名错误。lambda 的对应调用会对错误数量抛 ArgumentError。Proc 参数规则

这里的“宽松”有范围:不要推导为关键字要求、类型要求和业务约束都被取消。一个回调签名里包含必须关键字,仍应对实际错误调用单独测试;函数体中的数值操作也不会因为接收者是 Proc 就允许 nil。

lambda 也是 Proc 对象,lambda? 可以查询它的语义。经过 & 传递已有 Proc 时,还需要注意语义的保留;不能假定方法名写着 lambda 就能把任意非 lambda Proc 改造为严格回调。第 16 篇中的 define_method 又有方法定义自己的规则,需要独立验收。

严格数量与显式解构可以同时存在

strict.call([1, 2]) 传了一个位置参数,不满足两个位置参数的数量。本篇捕获这个 ArgumentError,并让未发生错误的情况失败。需要接收一个数组后再拆开,可以把拆解写进形参:

1
2
explicit = ->((a, b)) { [a, b] }
raise unless explicit.call([1, 2]) == [1, 2]

这个 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 交给产生一个数组的调用者,先保留数量失败,再改为显式解构;两条路径都要能够被断言识别。

参考资料

系列导航

导读 · 上一篇:05:方法与参数绑定 · 下一篇:07:闭包与非局部控制流 · 完整源码包