Ruby 块与 yield

这是 Ruby 的灵魂章节。块(Block)是 Ruby 最独特的设计之一——它让一段"无名代码"可以像参数一样被传递。配合 yieldProcLambda,你能写出非常优雅的迭代器、回调、DSL(领域专属语言)。Rails 的 has_manyvalidates 这些看起来像配置的语法,底层全是块。

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。判断方法: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]

最后一行的 &:upcaseRuby 最优雅的简写——:upcase 是符号,& 把符号转成 Proc,效果等同于带块的 map 调用。实战里到处都是。

小结

本章是 Ruby 的灵魂。块让"传递一段逻辑"变得自然,Proc/Lambda 让这段逻辑变成对象,yield 把控制权优雅地交还。理解了它,你就能读懂 Rails 源码里那些"魔法"——它们其实全是块。下一篇看字符串,把 Ruby 里最常用的数据类型方法过一遍。

← 上一篇 Ruby 方法

下一篇 Ruby 字符串

✈️💬