第11章 块
把block视为控制反转边界,掌握迭代、lifecycle、算法slot、yield参数与返回、执行控制、Proc封装以及局部和block变量作用域。
学习目标
- 能解释块的使用方法与隐藏常规处理在“第11章 块”中的责任边界
- 能围绕“怎样区分方法拥有的流程与块注入的策略、局部变量和返回语义?”运行正常与故障轨迹并定位首个分岔
- 能用“第11章 块的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”证明“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”
来源、版次与运行边界
“第11章 块”以作者维护的第 5 版支持页核定 2016 年 3 月 12 日首刷、两位作者、松本行弘监修以及四部分 23 章目录;逐章程序清单、练习答案和勘误只作为公开支持材料,不被冒充为原书全文。
对“第11章 块”而言,中文解释、示例、交互、练习和答案均为独立教学重写;站内中文章名是与官方 23 章顺序对应的课程映射,不宣称是日文小节的逐字翻译,也不从公开程序清单复制整段实现。
“第11章 块”固定在 Ruby 2.3 语境;Ruby 2.3.0 官方文档与稳定版发布说明用于核对当时可用的语言和标准库行为。现代 Ruby 的差异只能另列迁移说明,不能静默改变本页示例的版本结论。
围绕“怎样区分方法拥有的流程与块注入的策略、局部变量和返回语义?”,本页验收“第11章 块的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”。先预测正常轨迹,再只注入“把普通 Proc 中的 return 当作只离开 Proc,意外提前退出外层方法”;若无法定位第一处状态分岔,就拒绝当前解释。
从“block只是循环体”开始纠正
Block不只是each后面的一段代码;它让method保留整体流程,同时由caller注入一小段policy。统一解释iterator、File.open、sort_by与callback。
先预测:File.open(path) { |file| ... }中谁负责close?File.open method拥有resource lifecycle,即使block抛异常也应关闭;block只在handle有效期间工作。若caller自己open后忘记ensure,责任边界就泄漏。
块是什么
Block由{ ... }或do ... end附着在method call上,可接收parameters、读取outer locals并返回最后expression。每次call最多有一个implicit block;它不是普通positional argument,但可通过&与Proc互转。
Brace通常绑定更紧,do/end更松;logger items.map {}和logger items.map do ... end可能把block交给不同call。要求复杂调用加括号或分行。
Block object并不自动存在;只有捕获为Proc时才形成可保存callable。普通yield通常更直接。第21章会比较Proc/lambda arity与return语义,本章先固定implicit block contract。
块的使用方法:循环
Array#each拥有遍历order,block拥有每个element的动作;map拥有结果Array构造,block负责element projection;select拥有过滤流程,block提供predicate。帮助选对method。
records = [
{ name: "A", score: 80 },
{ name: "B", score: 95 },
]
names = records
.select { |record| record[:score] >= 90 }
.map { |record| record[:name] }
p namesChain的每一步都生成/传递shape;数据大时评估intermediate allocation并可用lazy。Block中若同时mutation外部state、I/O和复杂branch,应提取命名method或重新选择higher-level iterator,使policy可单测。
隐藏常规处理
Resource method可把setup/cleanup藏在稳定lifecycle中:File.open、Mutex#synchronize、transaction wrapper。这里“隐藏”不是不可观察,而是把每个caller都会重复且容易出错的mechanics集中实现。要求ensure覆盖所有exit。
def with_output(path)
file = File.open(path, "w:UTF-8")
yield(file)
ensure
file.close if file
endProduction还应处理atomic replace、fsync等domain要求;示例只展示close owner。Block返回值通常应透传,使wrapper不改变业务result;cleanup error不能无意遮蔽primary exception。
替换部分算法
Sort提供总体排序algorithm,block定义comparison或projection;group_by提供partition,block定义key;inject提供fold,block定义step。相比继承整套algorithm,block是窄而局部的strategy。
ordered = records.sort_by { |record| [-record[:score], record[:name]] }
grouped = records.group_by { |record| record[:score] >= 60 ? :pass : :retry }
p ordered: ordered, grouped: groupedComparison/projection应稳定、无重副作用且对相同input给一致结果;block内网络调用会被执行多次并使sort极慢。先预计算key或cache,明确error policy。
定义带块的方法
Method直接用yield调用当前block;没有block时yield抛LocalJumpError。block_given?可实现optional behavior,但要避免一个method实际上有两套无关API。是定义的核心。
执行块
def repeat(count)
raise ArgumentError, "block required" unless block_given?
count.times do |index|
yield(index)
end
end
repeat(3) { |index| puts index }Repeat拥有count validation与iteration,block只处理index。Method返回count.times的receiver;若API需要收集block results,应明确用Array构造,不能让caller猜。
传递块参数,获取块的值
Yield arguments按block parameter规则分配;block最后expression返回给method。Method可将结果用于map、validation或early stop。应覆盖arity与nil/false。
def first_accepted(values)
values.each do |value|
accepted = yield(value)
return value if accepted
end
nil
end
p first_accepted([1, 4, 7]) { |value| value.even? }这里block result按truth contract解释,method返回原value或nil。若value本身可nil,需要Result/value object区分found nil与not found。
控制块的执行
Method决定是否yield、次数和顺序,可在yield前后执行逻辑。Optional block可在无block时返回Enumerator,这是Ruby collection API常见模式:return enum_for(__method__, args) unless block_given?。Enumerator保留deferred execution,文档说明source是否一次性。
Block中的next把value返回本次yield,break退出yielding iterator call;Non-local return语义取决于普通block/Proc/lambda与定义context,不能在wrapper中随意假定。Resource method用ensure防护所有control exit。
将块封装为对象
&block在parameter中捕获implicit block为Proc,可保存、转发或稍后call;调用处&callable把to_proc结果作为block。避免不必要复杂度。
class Pipeline
def initialize(&transform)
raise ArgumentError, "transform required" unless transform
@transform = transform
end
def call(value)
@transform.call(value)
end
end
pipeline = Pipeline.new { |text| text.strip.downcase }
p pipeline.call(" Ruby ")Captured Proc closes over outer locals及其bindings,可能延长大对象lifetime;异步保存还要考虑thread safety与request context泄漏。若behavior有复杂state/lifecycle,独立strategy object比Proc更易观察、序列化和替换。
局部变量与块变量
Block是closure,可读取并rebind已存在的outer local。Block parameter如|item|只在block内;semicolon后的|item; scratch|声明block-local,即使外层已有scratch也不修改。防止偶然shadow/mutation。
total = 0
scratch = :outer
[1, 2, 3].each do |value; scratch|
scratch = value * 2
total += scratch
end
p total: total, scratch: scratch # 12, :outerParallel/multithread callbacks共享captured mutable binding时需要同步;closure并不复制值。长生命周期callback不要捕获整个request/service object,只提取必要immutable values,减少retention与隐藏dependency。
正式节点与章专属证据
- ↡在“第11章 块”中,块的使用方法必须连接输入、状态变化与可复核结果。 :第 1 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
- ↡在“第11章 块”中,隐藏常规处理必须连接输入、状态变化与可复核结果。 :第 2 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
- ↡在“第11章 块”中,替换部分算法必须连接输入、状态变化与可复核结果。 :第 3 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
- ↡在“第11章 块”中,定义带块的方法必须连接输入、状态变化与可复核结果。 :第 4 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
- ↡在“第11章 块”中,执行块必须连接输入、状态变化与可复核结果。 :第 5 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
- ↡在“第11章 块”中,传递块参数,获取块的值必须连接输入、状态变化与可复核结果。 :第 6 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
- ↡在“第11章 块”中,控制块的执行必须连接输入、状态变化与可复核结果。 :第 7 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
- ↡在“第11章 块”中,将块封装为对象必须连接输入、状态变化与可复核结果。 :第 8 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
- ↡在“第11章 块”中,局部变量与块变量必须连接输入、状态变化与可复核结果。 :第 9 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
对象与状态模型
从输入、接收者到可观察证据
怎样区分方法拥有的流程与块注入的策略、局部变量和返回语义?
输入与接收者
固定块的使用方法所需的原始值、Ruby 版本和调用入口。
状态变化
在执行前记录接收者身份,并声明隐藏常规处理的允许状态。
观察证据
保存第11章 块的初值、参数、编码或资源位置。
正式节点:块的使用方法、隐藏常规处理、替换部分算法、定义带块的方法、执行块、传递块参数,获取块的值、控制块的执行、将块封装为对象、局部变量与块变量
控制与消息轨迹
在相同初值下定位首个分岔
- 01固定块的使用方法的输入和接收者
- 02执行隐藏常规处理并记录状态
- 03观察替换部分算法的返回或副作用
- 04用局部变量与块变量核对不变量并复位
运行不变量:yield 次数、块参数、返回位置与外部状态变化都能逐步解释。
边界故障探针
一次只破坏一个前提
本章回顾:Method拥有流程,Block注入policy
- Block通过控制反转连接iteration、resource lifecycle、algorithm slot与callback,不只是循环syntax。
- Block attachment由parser决定,nested无括号调用必须明确block owner。
- 自定义method声明block required/optional、yield次数、参数、result、异常和ensure cleanup。
- Capture为Proc只用于保存、延迟、组合或转发;复杂有状态策略优先object。
- Block捕获outer bindings,parameter/block-local建立内部scope,control flow随block/Proc/lambda context变化。
练习与答案
练习
- 问题 1:建立正常轨迹。 回答“怎样区分方法拥有的流程与块注入的策略、局部变量和返回语义?”,并写出四步执行记录。
- 问题 2:注入单一故障。 只制造“把普通 Proc 中的 return 当作只离开 Proc,意外提前退出外层方法”,应从哪里开始定位?
- 问题 3:覆盖正式节点。 用一个证据包串联块的使用方法、隐藏常规处理、替换部分算法、定义带块的方法、执行块、传递块参数,获取块的值、控制块的执行、将块封装为对象、局部变量与块变量,说明为什么结论可由另一位读者独立复核。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 块的使用方法
“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 隐藏常规处理
“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 替换部分算法
“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 定义带块的方法
“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 执行块
“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 传递块参数,获取块的值
“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 控制块的执行
“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 将块封装为对象
“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 局部变量与块变量
“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。