Ruby 块与 yield
这是 Ruby 的灵魂章节。块(Block)是 Ruby 最独特的设计之一——它让一段"无名代码"可以像参数一样被传递。配合 yield、Proc、Lambda,你能写出非常优雅的迭代器、回调、DSL(领域专属语言)。Rails 的 has_many、validates 这些看起来像配置的语法,底层全是块。
1. 什么是块(Block)?
块是一段附属于方法调用的无名代码。任何方法调用都可以"带一个块"——至于用不用,由方法内部决定。
# 块:一段"无名代码",用 do/end 或花括号包裹
# do/end 习惯用于多行,花括号用于单行
3.times do |i|
puts "第 #{i} 次"
end
# 等价的单行写法
3.times { |i| puts "第 #{i} 次" }
# |i| 是块参数,块自己调用时由方法传值两种包裹方式的约定:do / end 用于多行、有"语句感"的块;花括号用于单行、有"返回值感"的块。社区基本遵守这条,代码风格统一。
2. yield:让方法"调用"块
方法内部用 yield 把控制权交给传进来的块。块不是方法的参数(至少在 Ruby 1.9 前),它是一种特殊的存在:
# yield:在方法内部"让出控制权"给传进来的块
def with_log
puts "before"
yield # 执行调用者传来的块
puts "after"
end
with_log { puts "doing work" }
# 输出:
# before
# doing work
# after
# yield 还能传参给块
def greet
yield "Alice"
yield "Bob"
end
greet { |name| puts "Hi, #{name}" }
# Hi, Alice
# Hi, Bob这种"我提供骨架,你提供细节"的模式,正是 Ruby 各种 DSL 的根基。比如 File.open 自动帮你关闭文件、Benchmark.measure 帮你计时——你只关心 yield 里的业务。
3. block_given?:检查块是否存在
有时候方法希望"有块就调用,没块也能跑"。用 block_given? 判断:
# yield 检测:用 block_given? 判断有没有块
def maybe_yield
if block_given?
yield
else
puts "no block"
end
end
maybe_yield { puts "has block" } # has block
maybe_yield # no block不检查直接 yield 而调用者没传块,会抛 LocalJumpError。所以公开方法里 yield 前最好检查。
4. Proc:把块"对象化"
块本身不是对象,不能存变量、不能传来传去。Proc 就是块的"对象版本"——把一段闭包打包,可以像数据一样传递:
# Proc:把块"对象化",可以存进变量、传来传去
double = Proc.new { |x| x * 2 }
puts double.call(5) # 10
puts double.(5) # 10(简写)
puts double[5] # 10(再简写)
# 用 proc 工厂方法更简洁
triple = proc { |x| x * 3 }
puts triple.call(4) # 12三种调用写法都等价。.call 最显式,[] 最像数组下标,看团队习惯。
5. Lambda:更严格的闭包
Lambda 是 Proc 的"严格表亲",语法上几乎一样,但参数检查更严、return 行为不同。Ruby 1.9+ 的箭头语法是它最常见的写法:
# Lambda:另一种闭包,语法像 Proc 但行为更"严格"
square = lambda { |x| x * x }
puts square.call(5) # 25
# 箭头语法(Ruby 1.9+,推荐)
square = ->(x) { x * x }
puts square.call(6) # 36
add = ->(a, b) { a + b }
puts add.call(2, 3) # 5箭头语法 ->(x) 读作"lambda of x",写起来比 Proc.new 紧凑得多,是写函数式风格代码的首选。
6. Proc vs Lambda:两大关键区别
这是面试和实战都会问的重点。一句话总结:Lambda 像方法,Proc 像块。
# Proc vs Lambda 的两大区别
# 区别 1:参数检查严格度
p = Proc.new { |a, b| [a, b] }
l = lambda { |a, b| [a, b] }
p.call(1, 2, 3) # [1, 2](多余参数被忽略,宽容)
p.call(1) # [1, nil](缺的补 nil)
l.call(1, 2, 3) # ArgumentError!(参数数量不对)
l.call(1) # ArgumentError!
# 区别 2:return 行为
def proc_return
p = Proc.new { return 10 }
p.call
puts "这行不会执行——Proc 的 return 直接跳出方法"
end
def lambda_return
l = lambda { return 10 }
l.call # Lambda 的 return 只跳出 lambda 自己
puts "这行会执行"
20
end- 参数检查:Lambda 严格(数量不对抛错),Proc 宽容(多余忽略、缺失补 nil)。
- return 行为:Lambda 的
return只退出 lambda 自己;Proc 的return会直接退出包含它的方法(因为块本来就属于那个方法)。
实战建议:需要"独立函数"用 Lambda,需要"块的形式"用 Proc。判断方法:pr.lambda? 返回 true/false。
7. &block 与 &:方法/块的相互转换
方法签名里的 &block 把传入的块捕获为 Proc 对象;调用时的 &proc 则反过来,把 Proc 转成块:
# &block:把块作为"有名参数"接收
def timer(&block)
start = Time.now
block.call
puts "耗时 #{Time.now - start} 秒"
end
timer { sleep 1 }
# 耗时 1.0 秒
# 反向 &:把 Proc 转成块传给方法
nums = [1, 2, 3]
double = proc { |x| x * 2 }
nums.map(&double) # [2, 4, 6]
# 最常见用法:把方法名符号转成块
["a", "b", "c"].map(&:upcase) # ["A", "B", "C"]
[1, 2, 3].select(&:odd?) # [1, 3]最后一行的 &:upcase 是Ruby 最优雅的简写——:upcase 是符号,& 把符号转成 Proc,效果等同于带块的 map 调用。实战里到处都是。
小结
本章是 Ruby 的灵魂。块让"传递一段逻辑"变得自然,Proc/Lambda 让这段逻辑变成对象,yield 把控制权优雅地交还。理解了它,你就能读懂 Rails 源码里那些"魔法"——它们其实全是块。下一篇看字符串,把 Ruby 里最常用的数据类型方法过一遍。
← 上一篇 Ruby 方法
下一篇 Ruby 字符串 →