如果说对象与消息是 Ruby 的地基,那么 Block 就是 Ruby 开始拥有气质的地方。
Ruby 的很多代码第一次看起来都很轻:
ruby
3.times do |index|
puts index
end
或者:
ruby
users.select { |user| user.active? }
甚至:
ruby
File.open("data.txt") do |file|
puts file.read
end
语法并不复杂,真正容易卡住的是:
do ... end到底属于谁?
|index|里的值是谁传进来的?
yield又是在做什么?
这一篇先不急着记术语。我们从"谁在调用谁"开始。
1. 从一段熟悉的集合处理开始
Ruby:
ruby
result = [1, 2, 3, 4, 5]
.select { |number| number > 2 }
.map { |number| number * 10 }
puts result
结果是:
text
30
40
50
如果你来自 Java,很容易把它映射成 Stream:
java
var result = List.of(1, 2, 3, 4, 5)
.stream()
.filter(number -> number > 2)
.map(number -> number * 10)
.toList();
JavaScript 也类似:
javascript
const result = [1, 2, 3, 4, 5]
.filter(number => number > 2)
.map(number => number * 10);
今天看来,很多语言都有"把一段行为传进去"的能力。
但 Ruby 很早就把这种表达方式放到了语言中心:
方法不只接收数据,也可以在调用时附带一段尚未执行的代码。
Ruby 把这段附带的代码叫做 Block。
不过如果只记住这句话,仍然很容易觉得 Block 很抽象。
所以先来看一次最简单的执行过程。
2. 别急着记定义:先看 Block 到底是谁在调用谁
先看:
ruby
3.times do |index|
puts "round #{index}"
end
第一次看到这段代码,可以先把它拆成两部分。
第一部分是普通的方法调用:
ruby
3.times
它的意思很普通:
text
对整数对象 3 调用 times 方法
第二部分是跟在这次方法调用后面的代码:
ruby
do |index|
puts "round #{index}"
end
这就是 Block。
可以先把整个调用想象成:
text
3.times
│
├── 调用 times 方法
│
└── 同时附带一段 Block
│
▼
┌─────────────────────┐
│ |index| │
│ puts "round ..." │
└─────────────────────┘
这里最重要的一点是:
不是 Block 自己决定执行三次,而是
times方法决定执行 Block 三次。
Block 只负责描述:
"如果你什么时候想执行我,我要做什么。"
而 times 决定:
"我要什么时候执行你、执行几次、每次给你什么参数。"
这就是 Ruby Block 最核心的控制关系。
3. |index| 到底是谁赋值的
继续看:
ruby
3.times do |index|
puts index
end
这里很容易产生一个误解:
ruby
|index|
看起来很像 times 方法的参数。
其实不是。
times 的方法调用本身仍然只是:
ruby
3.times
而:
ruby
|index|
是 Block 的参数列表。
真正的问题变成了:
谁把值塞进
index?
答案是:
times方法。
为了理解它,我们可以写一个"概念版"的 times。
注意,下面不是 Ruby 标准库 Integer#times 的真实实现,只是为了看清执行过程:
ruby
def my_times(number)
index = 0
while index < number
yield index
index += 1
end
end
调用:
ruby
my_times(3) do |index|
puts "round #{index}"
end
现在关键语句出现了:
ruby
yield index
第一次循环时:
ruby
yield 0
于是 Block 收到:
ruby
index = 0
然后执行:
ruby
puts "round 0"
第二次:
ruby
yield 1
于是:
ruby
index = 1
第三次同理。
整个过程可以画成:
text
调用 my_times(3),同时附带 Block
│
▼
进入 my_times
│
index = 0
│
yield 0
│
▼
执行调用者提供的 Block
|index|
0
│
puts "round 0"
│
▼
回到 my_times
│
index = 1
│
yield 1
│
▼
再执行一次 Block
│
......
所以以后看到:
ruby
something do |x|
...
end
可以先问自己:
x是谁通过yield传进来的?
通常一旦找到这个答案,Block 就不会那么神秘了。
4. yield:执行调用者附带的 Block
现在正式看 yield。
文章里如果只写一句:
yield会把控制权交给 Block。
这句话当然没错,但第一次接触时还是有点抽象。
可以先把它翻译成一句更直白的话:
yield= "就在这里,执行一下调用这个方法时附带的那段 Block。"
例如:
ruby
def hello
puts "before"
yield
puts "after"
end
调用:
ruby
hello do
puts "Jack"
end
执行过程是:
text
进入 hello
│
▼
puts "before"
│
▼
before
│
▼
yield
│
├── 暂时离开 hello
│
▼
执行 Block
puts "Jack"
│
▼
Jack
│
▼
Block 结束
│
└── 回到 yield 后面
│
▼
puts "after"
│
▼
after
最终输出:
text
before
Jack
after
所以初学时,你甚至可以在脑子里把:
ruby
yield
临时替换成:
text
【在这里执行调用者传进来的 Block】
那么:
ruby
def hello
puts "before"
yield
puts "after"
end
就变成:
text
before
【调用者的代码】
after
Block 的控制结构感就出来了。
5. 方法不仅能执行 Block,还能给 Block 传参数
yield 后面可以带参数:
ruby
def repeat(times)
times.times do |index|
yield index
end
end
调用:
ruby
repeat(3) do |index|
puts "round #{index}"
end
对应关系非常直接:
ruby
yield index
把值交给:
ruby
|index|
也可以传多个值:
ruby
def give_user
yield "Jack", 18
end
give_user do |name, age|
puts "#{name} is #{age} years old"
end
这里:
ruby
yield "Jack", 18
对应:
ruby
|name, age|
所以可以先记住一个非常实用的对应关系:
text
yield 后面的值
↓
Block 竖线里的参数
例如:
ruby
yield user
对应:
ruby
|user|
ruby
yield key, value
对应:
ruby
|key, value|
6. Block 也会产生返回值,而且返回给 yield
理解参数以后,还有另一半很关键:
Block 执行完以后,结果去了哪里?
看一个最简单的例子:
ruby
def calculate
result = yield 10
puts "Block returned: #{result}"
end
calculate do |number|
number * 2
end
执行到:
ruby
yield 10
Ruby 会执行:
ruby
number * 2
得到:
text
20
这个 20 会成为 yield 表达式的结果:
ruby
result = yield 10
因此:
ruby
result == 20
于是输出:
text
Block returned: 20
所以 Block 和方法之间其实是双向交流:
text
方法
│
│ yield 参数
▼
Block
│
│ 计算结果
▼
方法继续执行
这件事对于理解 select、map 非常重要。
7. 再看 select:这次把执行过程完整拆开
之前看到:
ruby
users.select { |user| user.active? }
现在已经可以一点点拆了。
可以想象一个简化版 select:
ruby
def my_select(array)
result = []
for item in array
keep = yield item
if keep
result << item
end
end
result
end
调用:
ruby
active_users = my_select(users) do |user|
user.active?
end
假设:
text
users = [Tom, Jack, Lucy]
第一轮:
text
item = Tom
│
▼
yield Tom
│
▼
|user| 收到 Tom
│
▼
Tom.active?
│
▼
true
│
▼
yield 的结果就是 true
│
▼
把 Tom 放进 result
第二轮:
text
item = Jack
↓
yield Jack
↓
Jack.active?
↓
false
↓
不保留
第三轮同理。
最终:
text
[Tom, Lucy]
所以:
ruby
users.select { |user| user.active? }
真正表达的是一种合作:
text
select:
"遍历、保存结果这些事情我负责。"
Block:
"这个 user 到底该不该留下,我负责判断。"
这就是 Ruby 很喜欢的设计方式:
方法掌握流程,Block 填入变化的行为。
8. 再看 map:方法定义流程,Block 定义转换规则
map 也是同一个模型。
ruby
numbers = [1, 2, 3]
result = numbers.map do |number|
number * 10
end
可以想象成:
ruby
def my_map(array)
result = []
for item in array
mapped = yield item
result << mapped
end
result
end
于是:
text
1 → yield 1 → 10 → 放进 result
2 → yield 2 → 20 → 放进 result
3 → yield 3 → 30 → 放进 result
得到:
ruby
[10, 20, 30]
现在再回头看文章开头:
ruby
result = [1, 2, 3, 4, 5]
.select { |number| number > 2 }
.map { |number| number * 10 }
其实就是两次"方法控制流程 + Block 提供行为":
text
select
│
└── 你告诉它:谁应该留下
↓
[3,4,5]
│
▼
map
│
└── 你告诉它:每个元素怎么转换
↓
[30,40,50]
9. Block 究竟是什么:现在再回来记定义
到这里再看定义,会容易很多。
Block 是跟随一次方法调用的一段代码:
ruby
3.times do |index|
puts index
end
也可以写成花括号:
ruby
3.times { |index| puts index }
惯例上:
- 多行逻辑常用
do ... end; - 单行逻辑常用
{ ... }。
二者在 Ruby 中还存在结合优先级差异,但初学阶段先遵循这个惯例即可。
Block 参数写在竖线中:
ruby
|index|
现在可以更准确地定义:
Block 是附属于一次方法调用的特殊代码结构。方法可以通过
yield执行它、给它参数,并接收它的返回值。
还有一个重要边界:
Block 本身不是一个普通 Ruby 对象。
因此你不能像普通对象那样直接把一个裸 Block 随意存进变量。
如果想把"这段行为"保存起来、以后再调用,就会进入后面的 Proc。
所以不要急着把:
text
Block = Java Lambda
直接画等号。
它们有相似的思想,但 Ruby 的 Block 首先是一种依附于方法调用的语言结构。
10. Block 的真正力量:控制反转
传统循环:
java
for (int i = 0; i < 3; i++) {
System.out.println(i);
}
循环流程由调用者自己写。
Ruby:
ruby
3.times do |i|
puts i
end
这里调用者不再自己维护:
text
初始值
循环条件
递增
结束判断
这些都交给 times。
你只提供:
ruby
puts i
也就是说:
text
传统方式:
调用者控制整个流程
Block 方式:
方法控制流程
调用者只填入变化部分
这是一种很小的"控制反转"。
它也解释了为什么 Ruby 的很多 API 看起来像在说话:
ruby
3.times do
retry_request
end
ruby
users.each do |user|
send_email(user)
end
ruby
File.open("data.txt") do |file|
process(file)
end
共同模式都是:
"框架性的流程由方法负责,业务行为由 Block 提供。"
11. block_given?:Block 可以是可选的
如果方法直接执行:
ruby
yield
但调用者没有提供 Block,就会报错。
所以 Ruby 提供:
ruby
block_given?
例如:
ruby
def log(message)
detail = block_given? ? yield : ""
puts "#{message} #{detail}"
end
既可以:
ruby
log("request completed")
也可以:
ruby
log("request completed") do
"status=200"
end
第二种情况下:
ruby
yield
会得到:
text
"status=200"
于是输出详细信息。
在工程代码中,Block 常被用于:
- 集合遍历与转换;
- 资源管理;
- 事务边界;
- 重试与超时;
- 回调;
- DSL 配置;
- 测试描述。
12. Ruby 的资源管理:把样板代码包在 Block 外面
现在再看:
ruby
File.open("data.txt", "r") do |file|
puts file.read
end
这段代码就没那么神秘了。
你可以把 File.open 的思想粗略想象成:
ruby
def open_file(path)
file = 打开文件(path)
yield file
关闭文件(file)
end
真实实现当然更完善,比如需要保证异常情况下也能正确清理资源,但控制结构就是类似的。
于是调用者只需要描述:
ruby
puts file.read
而 File.open 负责:
text
打开文件
↓
把 file 交给 Block
↓
执行调用者的代码
↓
Block 结束
↓
关闭文件
Java 的 try-with-resources:
java
try (var reader = Files.newBufferedReader(path)) {
System.out.println(reader.readLine());
}
C++ 的 RAII 则依靠对象生命周期释放资源。
三者都在表达资源边界,只是选择不同:
| 语言 | 主要机制 | 边界由什么表达 |
|---|---|---|
| Ruby | 方法 + Block + ensure 等机制 |
Block 的执行范围 |
| Java | AutoCloseable + try-with-resources |
特殊语法块 |
| C++ | RAII + 确定性析构 | 对象生命周期 |
Ruby 的方式尤其适合把:
text
前置
↓
执行用户代码
↓
清理
包装成一个普通方法。
例如计时:
ruby
def with_timing
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
result = yield
result
ensure
finished_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
puts "elapsed: #{finished_at - started_at}s"
end
调用:
ruby
result = with_timing do
expensive_operation
end
执行结构就是:
text
开始计时
↓
执行你的 Block
↓
结束计时
所以 Block 不只是一个回调。
它经常直接成为控制结构的一部分。
13. 再做一个微型事务:真正感受"方法包住 Block"
假设我们想要这种调用方式:
ruby
with_transaction do
create_order
decrease_inventory
end
先不连接真正数据库,只打印事务边界:
ruby
def with_transaction
puts "BEGIN"
yield
puts "COMMIT"
rescue => error
puts "ROLLBACK"
raise error
end
如果 Block 正常结束:
text
BEGIN
create_order
decrease_inventory
COMMIT
如果中间抛异常:
text
BEGIN
↓
执行 Block
↓
出现异常
↓
ROLLBACK
这里值得特别注意:
调用者写的是:
ruby
create_order
decrease_inventory
但真正包围它们的是方法内部的:
text
BEGIN
...
COMMIT / ROLLBACK
这正是 Block 在 Ruby 框架设计中非常强大的原因。
14. 到这里先做一次停顿:你应该掌握什么
在继续 Proc 之前,最好先确认自己能够解释下面这段:
ruby
def repeat(times)
times.times do |index|
yield index
end
end
repeat(3) do |index|
puts index
end
至少能回答四个问题:
repeat(3)是一次普通方法调用;do ... end是附在这次调用上的 Block;yield index会执行这个 Block,并把index传进去;- Block 执行完之后,程序会回到
yield后面继续。
再进一步:
ruby
result = yield value
说明:
Block 自己最后计算出来的值,还能返回给方法。
如果这些已经顺畅,后面的 Proc、Lambda 和 & 就只是:
"怎么把这段行为保存起来,以及怎么在 Block 和对象之间转换。"
而不再是一堆彼此孤立的语法。
15. 从 Block 到 Proc:让行为成为对象
Block 很方便,但它有一个边界:
Block 依附于一次方法调用,本身不是普通对象。
如果我们想把行为保存到变量里:
ruby
doubler = ???
然后以后再执行:
ruby
doubler.call(10)
这时候就需要 Proc。
ruby
doubler = Proc.new { |number| number * 2 }
puts doubler.call(10)
# 20
也可以写:
ruby
doubler = proc { |number| number * 2 }
Proc 是一个真正的 Ruby 对象,所以可以:
- 赋值给变量;
- 放进数组或 Hash;
- 作为参数传递;
- 从方法返回;
- 在之后调用。
例如:
ruby
operations = {
double: proc { |n| n * 2 },
square: proc { |n| n * n }
}
puts operations[:square].call(5)
# 25
现在可以得到第一张关系图:
text
Block
│
│ 依附于一次方法调用
│
│ 如果需要对象化
▼
Proc
│
├── 可以保存
├── 可以传递
└── 可以稍后 call
16. &block:把 Block 显式捕获成 Proc
前面我们一直使用:
ruby
yield
方法其实可以不把 Block 存进变量,直接执行它。
但如果我们确实希望在方法内部把这个 Block 当成对象使用,可以这样:
ruby
def execute(&block)
block.call
end
调用:
ruby
execute do
puts "running"
end
这里最重要的是:
ruby
&block
不是说参数名必须叫 block。
真正有作用的是:
ruby
&
它让 Ruby 把调用时附带的 Block 转成一个 Proc,然后赋值给局部变量:
ruby
block
所以:
ruby
def execute(&block)
block.call
end
和前面的:
ruby
def execute
yield
end
都能执行调用者给出的 Block。
区别在于:
text
yield
↓
直接执行 Block
&block
↓
先把 Block 变成 Proc 对象
↓
然后可以 block.call、保存、继续传递......
如果只是立即执行,通常:
ruby
yield
更简单。
如果需要把它当对象处理,才更需要:
ruby
&block
17. & 的反方向:把 Proc 当成 Block 传进去
& 不只可以把 Block 捕获成 Proc。
反过来,如果手里已经有 Proc:
ruby
doubler = proc { |number| number * 2 }
可以:
ruby
numbers = [1, 2, 3]
result = numbers.map(&doubler)
这里:
ruby
&doubler
表示:
把这个 Proc 转换成这次
map调用所需要的 Block。
所以 & 可以先记成一座桥:
text
&
Block <────────────────> Proc
方法调用附带的代码结构 可保存、可传递的对象
两个方向分别是:
ruby
def execute(&block)
text
Block → Proc
和:
ruby
numbers.map(&doubler)
text
Proc → Block
这就是后面理解 &:active? 的基础。
18. Lambda:一种更像"独立函数"的 Proc
Ruby Lambda 可以写成:
ruby
adder = lambda { |a, b| a + b }
也可以用更常见的箭头形式:
ruby
adder = ->(a, b) { a + b }
调用:
ruby
adder.call(1, 2)
Lambda 本质上仍然属于 Proc 家族,但语义更接近独立函数。
最需要知道的两个差异是:
- 参数数量检查;
return的行为。
18.1 参数数量
Lambda 更严格:
ruby
strict = ->(a, b) { a + b }
strict.call(1)
# ArgumentError
普通 Proc 相对宽松:
ruby
loose = proc { |a, b| [a, b] }
loose.call(1)
# [1, nil]
18.2 return 的范围
Lambda 中的:
ruby
return
只从 Lambda 自己返回。
而普通 Proc 中的 return 会试图从定义它的外层方法返回;如果那个外层方法上下文已经不存在,还可能抛出 LocalJumpError。
这件事暂时不用死背每个边界案例。
先记住它表达出的设计差异:
Lambda 更像一个独立函数;普通 Proc 更像从外层控制流中截下来的一段代码。
19. 闭包:行为会记得它诞生时的环境
看这个例子:
ruby
def multiplier(factor)
->(number) { number * factor }
end
double = multiplier(2)
triple = multiplier(3)
puts double.call(10)
# 20
puts triple.call(10)
# 30
最有意思的是:
ruby
multiplier(2)
早就已经执行结束了。
但是:
ruby
double
这个 Lambda 以后再执行时,仍然记得:
ruby
factor == 2
这就是闭包。
可以把它理解成:
一段可调用的行为,不只携带代码,也携带它定义时能看到的词法环境。
JavaScript 中也非常常见:
javascript
function multiplier(factor) {
return number => number * factor;
}
Java Lambda 也能捕获 effectively final 的局部变量。
不同语言的限制和实现方式不同,但"行为连同环境一起保存"这一思想是相通的。
20. 终于回到 &:active?:现在它不应该再神秘了
之前看到:
ruby
users.select(&:active?)
第一次看非常容易卡在:
ruby
&:
现在拆开。
先看正常 Block:
ruby
users.select do |user|
user.active?
end
或者:
ruby
users.select { |user| user.active? }
Ruby 提供了一种常见简写:
ruby
users.select(&:active?)
其中:
ruby
:active?
是一个 Symbol。
而:
ruby
&
要求 Ruby 把后面的对象转换成适合作为 Block 使用的可调用对象。
Symbol 提供了 to_proc,于是:
ruby
:active?.to_proc
会得到一个 Proc,它的行为大致类似:
ruby
proc { |object| object.active? }
因此:
ruby
users.select(&:active?)
大致可以沿着下面的路线理解:
text
:active?
↓
Symbol#to_proc
↓
proc { |object| object.active? }
↓
& 把 Proc 作为 Block 交给 select
↓
users.select { |user| user.active? }
所以现阶段最实用的记忆方式仍然是:
ruby
collection.xxx(&:method_name)
大致等价于:
ruby
collection.xxx { |item| item.method_name }
例如:
ruby
users.map(&:name)
等价于:
ruby
users.map { |user| user.name }
ruby
numbers.select(&:even?)
等价于:
ruby
numbers.select { |number| number.even? }
ruby
names.map(&:upcase)
等价于:
ruby
names.map { |name| name.upcase }
这里终于可以把前面的所有概念串起来:
text
Block
↓
Proc
↓
& 转换
↓
Symbol#to_proc
↓
&:method_name
但不要为了追求短而到处使用 &:method。
当 Block 中有多个步骤、额外参数、条件判断,或者显式变量名更能表达业务意义时:
ruby
users.select do |user|
user.active? && user.age >= 18
end
通常比强行压缩更清晰。
21. 与 Java Lambda、Stream 的联系
Java:
java
users.stream()
.filter(User::isActive)
.map(User::getName)
.toList();
Ruby:
ruby
users
.select(&:active?)
.map(&:name)
两者都在描述:
text
哪些元素保留
↓
每个元素如何转换
而不是手动维护:
text
循环变量
临时数组
下标
append
从思想上说,都在做一件事情:
把"变化的行为"交给一个已经掌握流程的抽象。
Java 中可能是:
text
Stream.filter
Stream.map
Ruby 中可能是:
text
Enumerable#select
Enumerable#map
22. 与 Reactor 的联系:重点不是语法,而是控制权
再看 Reactor:
java
Mono.zip(userMono, orderMono)
.flatMap(tuple -> buildResult(tuple.getT1(), tuple.getT2()));
Ruby Block 和 Reactor 当然不是同一种机制。
但是它们可以训练一种相似的思维:
不要只盯着"这一行马上做什么",还要看"我正在把什么行为交给谁,由谁决定什么时候执行"。
在 Ruby:
ruby
users.select do |user|
user.active?
end
是 select 控制遍历,并决定什么时候执行 Block。
在 Reactor:
java
mono.flatMap(value -> ...)
你提供一段函数,但真正什么时候执行,会受到响应式订阅和信号流控制。
两者不是技术等价,而是共享一个更宽泛的思想:
行为可以被描述、传递、组合,并由另一个抽象决定何时执行。
理解这一点之后,flatMap 也不再只是一个难记的 API 名字。
23. Block 为什么能孕育 DSL
看一段 Ruby 配置:
ruby
database do
adapter :postgresql
pool 10
timeout 5
end
如果你现在已经理解 Block,就可以拆开:
ruby
database do
...
end
首先是调用:
ruby
database
然后给它一个 Block。
Block 里面:
ruby
adapter :postgresql
pool 10
timeout 5
又都是方法调用。
由于 Ruby 允许省略很多括号,普通方法调用和 Block 嵌套在一起以后,就会逐渐长得像一门专门的配置语言。
测试框架也是如此:
ruby
describe User do
it "has a name" do
expect(user.name).to eq("Jack")
end
end
Ruby 编译器并不认识什么特殊关键字:
text
describe
it
expect
它们本质上仍然是方法。
Block 让这些普通方法获得了嵌套结构和控制范围,于是框架就可以创造出非常接近自然语言的 DSL。
Ruby 的魅力之一就在这里:
很多看起来像"语言魔法"的东西,并不是编译器增加了新语法,而是普通方法、Block、对象模型组合后的结果。
24. 一张完整一点的心智地图
| 概念 | 现在应该怎样理解 |
|---|---|
| Block | 附在一次方法调用上的代码结构 |
| ` | x |
yield |
在方法中执行调用者提供的 Block |
yield value |
执行 Block,并把 value 传给 Block 参数 |
| Block 返回值 | Block 最后表达式的结果会成为 yield 的结果 |
block_given? |
判断当前方法调用有没有提供 Block |
| Proc | 对象化后的可调用行为,可以保存和传递 |
&block |
把调用时的 Block 捕获为 Proc |
&proc |
把 Proc 作为一次方法调用的 Block 传入 |
| Lambda | 一种参数与返回语义更像独立函数的特殊 Proc |
| 闭包 | 可调用行为会携带定义时能看到的环境 |
&:name |
借助 Symbol#to_proc 生成常见的单方法 Block 简写 |
如果还想再压缩成一句话:
Block 是调用者交给方法的一段行为;方法通过
yield决定什么时候执行它。Proc 则是把这段行为对象化。
25. 本篇练习:不要只练语法,要画执行过程
练习一:预测输出
ruby
def wrapper
puts "A"
yield
puts "B"
end
wrapper do
puts "C"
end
不要先运行,自己画:
text
进入谁
↓
执行到 yield
↓
跳到哪里
↓
Block 结束后回哪里
再预测输出顺序。
练习二:自己实现 repeat
ruby
def repeat(times)
# TODO
end
repeat(3) do |index|
puts "round #{index}"
end
目标输出:
text
round 0
round 1
round 2
重点不是循环本身,而是搞清楚:
ruby
yield index
和:
ruby
|index|
之间的关系。
练习三:自己实现一个简化版 select
ruby
def my_select(array)
# TODO
end
numbers = [1, 2, 3, 4, 5]
result = my_select(numbers) do |number|
number > 2
end
p result
目标:
ruby
[3, 4, 5]
这道题如果能独立写出来,说明你已经真正理解:
text
yield 参数
Block 参数
Block 返回值
方法继续处理返回值
这一整条链。
练习四:事务边界
实现:
ruby
with_transaction do
create_order
decrease_inventory
end
正常时打印:
text
BEGIN
COMMIT
Block 抛异常时打印:
text
BEGIN
ROLLBACK
然后思考:
- Java/Spring 的
@Transactional把同样的边界放在哪里? - Ruby Block 方案更显式还是更隐式?
- 注解、AOP、Block 都能消除样板代码,它们分别把控制权交给了谁?
结尾:先理解控制流,再理解语法糖
第一次看 Ruby 时,最容易犯的错误之一,就是看到:
ruby
users.select(&:active?)
马上开始研究:
text
& 是什么?
: 是什么?
to_proc 又是什么?
结果越挖越深,却忘了最原始的形式:
ruby
users.select do |user|
user.active?
end
而真正应该先理解的是:
text
select 控制遍历
↓
yield 把 user 给 Block
↓
Block 判断 active?
↓
true / false 返回给 select
↓
select 决定是否保留
当这条控制流已经变成本能之后:
ruby
&:active?
就只剩一层很薄的语法糖。
Block 也不再是一块需要死记的 Ruby 语法,而会逐渐变成一种设计直觉:
把稳定的流程留在方法里,把变化的行为留给调用者。
下一篇,我们会继续沿着这条线,看 Ruby 如何利用开放类、动态方法与 Block,让普通语言能力逐渐长成 Rails 那种近似魔法的 DSL。