大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
方法与 Blocks
这一课你将能做到
- 定义带默认参数、关键字参数、可变参数的方法,理解参数传递是引用传递
- 熟练使用代码块(block)、yield、Proc 和 lambda,说清三者的区别
- 用方法链和 tap/then 写出优雅的 Ruby 流式处理代码
为什么要学
Block 是 Ruby 最具标志性的特性——每个 Ruby 程序员天天写 array.each { ... },但很少有人能清晰地说出 block、Proc、lambda 的区别。DSL(如 RSpec 的 it { ... })和 Rails 的 scope 全都建立在 block 之上。block 理解不透,看高级 Ruby 代码等于看天书。
人话:方法是「命名了的代码块」,block 是「没有名字、直接跟在方法调用后的代码块」。Ruby 的独特之处在于:方法可以把 block 抓过来,用 yield 执行——这让你写出 with_open_file { ... } 这种结构,不用学 Java 的 try-with-resources。
先建立图像
方法 → 容器,给代码命名
│
▼ 调用时可选地附加一段匿名代码
Block → 用 {} 或 do...end 包裹的匿名代码
│
├─ yield:方法里执行附加的 block
├─ Proc:用 &block 捕获为对象,可复用、可传给其他方法
└─ lambda:特殊的 Proc,参数校验更严格
跟着做
步骤 1:定义方法与参数风格
# 普通参数 + 默认值
def greet(name, greeting = "你好")
"#{greeting},#{name}!"
end
puts greet("小明") # => 你好,小明!
puts greet("Lisa", "Hello") # => Hello,Lisa!
# 关键字参数(Ruby 2.0+)——调用时语义明确,不怕顺序换
def create_user(name:, age:, role: "user")
{ name: name, age: age, role: role }
end
puts create_user(name: "Tom", age: 25).inspect
# => {:name=>"Tom", :age=>25, :role=>"user"}
# 可变参数:数组形式
def sum(*numbers)
numbers.inject(:+)
end
puts sum(1, 2, 3, 4) # => 10
puts sum() # => nil(空数组 inject 没初始值返回 nil)
# 双星号 Hash 参数
def log(message, **options)
puts "[#{options[:level] || :info}] #{message}"
puts "标签: #{options[:tags].join(', ')}" if options[:tags]
end
log("启动完成", level: :warn, tags: ["startup", "main"])
# 方法返回最后一个表达式的值,return 可选
def double(n)
n * 2 # 返回值
end
puts double(7) # => 14
步骤 2:Block 与 yield
# yield:方法里执行调用时附加的 block
def with_timer
start = Time.now
yield # 执行 block
elapsed = Time.now - start
puts "耗时 #{elapsed.round(4)} 秒"
end
with_timer do
sleep(0.1)
puts "做了一件大事"
end
# 输出:做了一件大事
# 耗时 0.1001 秒
# yield 传值给 block
def each_twice(array)
array.each { |item| yield item }
end
each_twice([1, 2, 3]) do |n|
puts n * 10
end
# 输出 10, 20, 30
# block_given? 检查调用时是否传了 block
def smart_greet(name)
if block_given?
yield(name)
else
"你好,#{name}"
end
end
puts smart_greet("小明") # => 你好,小明
puts smart_greet("小明") { |n| "哈啰 #{n}" } # => 哈啰 小明
步骤 3:Proc 与 lambda
# Proc 对象:把 block 存成变量,可复用、可传给其他方法
say_hi = Proc.new { |name| "Hi, #{name}" }
puts say_hi.call("World") # => Hi, World
puts say_hi["World"] # 简写形式
# lambda:语法糖更简洁,参数校验更严格
say_hello = ->(name) { "Hello, #{name}" }
puts say_hello.call("Ruby") # => Hello, Ruby
# 关键区别 1:参数个数
proc_demo = proc { |a, b| "a=#{a}, b=#{b}" }
lambda_demo = ->(a, b) { "a=#{a}, b=#{b}" }
puts proc_demo.call(1) # => a=1, b= (Proc 缺少的参数填 nil)
# puts lambda_demo.call(1) # => ArgumentError(lambda 必须严格匹配)
# 关键区别 2:return 行为
# lambda 的 return 只退出 lambda 自身
# Proc 的 return 从定义 Proc 的「外层方法」退出(容易出 bug,少用)
# block 转 Proc:方法参数加 &
def run_twice(&block)
block.call
block.call
end
run_twice { puts "Running!" }
# => Running!
# => Running!
# Proc 转 block:方法调用时加 &
my_proc = ->(x) { x * x }
puts [1, 2, 3].map(&my_proc).inspect # => [1, 4, 9]
步骤 4:方法链与流式处理
# tap:「偷看」对象并继续返回原对象——构造复杂对象时排布初始化步骤
user = { name: "Tom" }.tap do |h|
h[:age] = 30
h[:role] = :admin
end
puts user.inspect # => {:name=>"Tom", :age=>30, :role=>:admin}
# then(Ruby 2.6+):把前一步结果传给 block,做链式变换
result = "hello"
.upcase
.then { |s| s + " WORLD" }
.then { |s| s.split(" ") }
puts result.inspect # => ["HELLO", "WORLD"]
# 对比不用 then 的写法(嵌套括号地狱)
puts "hello".upcase.then { |s| s + " WORLD" }.split(" ").inspect
讲清楚要点
- 参数传递是引用传递:传进方法的是对象的引用。对可变对象(String/Array/Hash)原地修改会影响外部;重新赋值则不影响。和 Java 的对象参数完全一致。
- 关键字参数 vs 位置参数:Ruby 3.0 强制关键字参数和 Hash 参数分离。调用
foo(a: 1)时,方法签名必须是def foo(a:)或def foo(**opts),不能混用。 - block 是语法结构,不是对象:
{ ... }和do ... end只是语法,不能直接赋值给变量。要变成对象,用Proc.new、proc或lambda。 - lambda 是「真正的匿名方法」:参数检查严格、return 只退出自身;Proc 是「松散的代码片段」,参数缺失补 nil、return 会从外部方法跳出。优先用 lambda,除非你需要 Proc 的宽松行为。
&的双向转换:方法参数里&block把调用者的 block 转成 Proc 对象;方法调用时&proc把 Proc 转回 block 传给方法。这是 Ruby 语法里最容易忽略也最精妙的符号之一。
你可能会懵的地方
-
{}和do ... end有优先级差异? 有。{}优先级高于do ... end。所以foo a { ... }里的 block 是给 a 的,不是给 foo 的。多行用do ... end,单行用{}。 -
为什么要区分 block/Proc/lambda? block 是「即用即走」,没法保存和传递;Proc 是可传递的 block;lambda 是有严格语义的匿名函数。这给了开发者三个粒度的控制。
-
return在 block 里表现诡异?return在 block(不是 lambda)里会从定义该 block 的方法返回,不是从 block 返回。这是设计选择——让 block 能当「内部控制流」用,但初学者极易踩坑。安全做法:block 里不用 return,最后一行即返回值。 -
方法可以没有括号调用? Ruby 允许
puts "hi"不写括号,这是 DSL 风格的根基。但嵌套调用时建议写括号避免歧义:foo(bar)而不是foo bar。 -
tap和then的区别?tap返回「调用 tap 的原对象」——用于初始化步骤排布;then返回 block 的返回值——用于链式数据变换。混用tap和then是写出高级 Ruby 代码的关键。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 方法里直接修改参数引用 | 外部对象被篡改 | 用 dup/clone 复制或不要原地修改 |
def foo(a, b:) 却 foo(1, 2) | Ruby 3 关键字参数独立 | foo(1, b: 2) |
| block 里写 return 想退出 block | 退出整个外层方法 | block 不用 return,最后一行即值 |
map(&my_method) 报错 | 需要对象响应 to_proc | 传 block:map { |x| my_method(x) } |
| Proc 当 lambda 用传错参数数 | Proc 不校验参数 | 要严格校验用 lambda |
卡点急救
wrong number of arguments:Ruby 3 后关键字参数和位置参数分离,检查调用方式和方法签名是否对齐。LocalJumpError: no block given:方法里写了 yield 但调用时没给 block。加block_given?保护,或确保调用带 block。- 方法返回值不对:Ruby 方法返回最后一个表达式的值,不是 return 关键字(除非显式写了 return)。检查方法最后一行是什么。
面试常问
-
block、Proc、lambda 的区别? block 是语法,不能独立存在;Proc 是 block 的对象化封装;lambda 是严格版 Proc。核心差异在参数校验和 return 语义:lambda 参数严格、return 只退出自身;Proc 宽松、return 会退出定义它的外部方法。
-
Ruby 的参数传递是值传递还是引用传递? 引用传递(pass-by-reference)。传的是对象引用,对可变对象原地修改会影响外部。重新赋值局部参数不影响外部——但这和 Java 的对象参数行为完全一致。
-
tap和then分别在什么场景使用?tap用于构造对象时插入副作用(初始化步骤),返回原对象;then用于流式数据变换链,返回变换结果。两者配合map/select使用是 Ruby 高级技巧。
自测三问
yield和block_given?分别做什么?- 为什么 lambda 比 Proc 更适合做「匿名函数」?
tap和then谁返回原对象、谁返回 block 的返回值?
小结一句话
方法是命名的代码,block 是匿名的代码,yield 是连接两者的桥梁;用 Proc/lambda 把 block 变成可复用的对象,Ruby 的代码表达力就上去了。
延伸
- Ruby Proc 与 lambda 官方文档
- 下一课:OOP 与 Mixin——理解 Ruby 的面向对象和模块混入。