第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 names

Chain的每一步都生成/传递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
end

Production还应处理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: grouped

Comparison/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, :outer

Parallel/multithread callbacks共享captured mutable binding时需要同步;closure并不复制值。长生命周期callback不要捕获整个request/service object,只提取必要immutable values,减少retention与隐藏dependency。

正式节点与章专属证据

  • :第 1 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
  • :第 2 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
  • :第 3 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
  • :第 4 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
  • :第 5 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
  • :第 6 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
  • :第 7 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
  • :第 8 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。
  • :第 9 个正式节点要能回到“yield 次数、块参数、返回位置与外部状态变化都能逐步解释。”,并说明故障发生前后的第一处差异。

对象与状态模型

从输入、接收者到可观察证据

怎样区分方法拥有的流程与块注入的策略、局部变量和返回语义?

输入与接收者

固定块的使用方法所需的原始值、Ruby 版本和调用入口。

状态变化

在执行前记录接收者身份,并声明隐藏常规处理的允许状态。

观察证据

保存第11章 块的初值、参数、编码或资源位置。

正式节点:块的使用方法、隐藏常规处理、替换部分算法、定义带块的方法、执行块、传递块参数,获取块的值、控制块的执行、将块封装为对象、局部变量与块变量

控制与消息轨迹

在相同初值下定位首个分岔

  1. 01固定块的使用方法的输入和接收者
  2. 02执行隐藏常规处理并记录状态
  3. 03观察替换部分算法的返回或副作用
  4. 04用局部变量与块变量核对不变量并复位

运行不变量:yield 次数、块参数、返回位置与外部状态变化都能逐步解释。

边界故障探针

一次只破坏一个前提

基线满足:yield 次数、块参数、返回位置与外部状态变化都能逐步解释。

本章回顾:Method拥有流程,Block注入policy

  1. Block通过控制反转连接iteration、resource lifecycle、algorithm slot与callback,不只是循环syntax。
  2. Block attachment由parser决定,nested无括号调用必须明确block owner。
  3. 自定义method声明block required/optional、yield次数、参数、result、异常和ensure cleanup。
  4. Capture为Proc只用于保存、延迟、组合或转发;复杂有状态策略优先object。
  5. Block捕获outer bindings,parameter/block-local建立内部scope,control flow随block/Proc/lambda context变化。

练习与答案

练习

  1. 问题 1:建立正常轨迹。 回答“怎样区分方法拥有的流程与块注入的策略、局部变量和返回语义?”,并写出四步执行记录。
  1. 问题 2:注入单一故障。 只制造“把普通 Proc 中的 return 当作只离开 Proc,意外提前退出外层方法”,应从哪里开始定位?
  1. 问题 3:覆盖正式节点。 用一个证据包串联块的使用方法、隐藏常规处理、替换部分算法、定义带块的方法、执行块、传递块参数,获取块的值、控制块的执行、将块封装为对象、局部变量与块变量,说明为什么结论可由另一位读者独立复核。

名词解释

名词解释

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

块的使用方法

“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

隐藏常规处理

“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

替换部分算法

“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

定义带块的方法

“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

执行块

“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

传递块参数,获取块的值

“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

控制块的执行

“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

将块封装为对象

“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

局部变量与块变量

“第11章 块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

资料与写作方式声明

本章以Ruby 基础教程第 5 版作者支持资料合法公开试读核定可见范围,并以目录限定未公开部分,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…