CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Ruby 语言入门/课时/方法与 Blocks
RawBlame
课时目录12 节

Ruby 语言入门

  1. 01Ruby 环境
  2. 02数据类型
  3. 03语法基础
  4. 04控制流
  5. 05方法与 Blocks
  6. 06OOP 与 Mixin
  7. 07元编程
  8. 08IO 与 Enumerable
  9. 09并发
  10. 10Gems 与框架
  11. 11测试与质量
  12. 12综合练习

方法与 Blocks

yield/proc/lambda。

30 分钟

方法与 Blocks

这一课你将能做到

  1. 定义带默认参数、关键字参数、可变参数的方法,理解参数传递是引用传递
  2. 熟练使用代码块(block)、yield、Proc 和 lambda,说清三者的区别
  3. 用方法链和 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

讲清楚要点

  1. 参数传递是引用传递:传进方法的是对象的引用。对可变对象(String/Array/Hash)原地修改会影响外部;重新赋值则不影响。和 Java 的对象参数完全一致。
  2. 关键字参数 vs 位置参数:Ruby 3.0 强制关键字参数和 Hash 参数分离。调用 foo(a: 1) 时,方法签名必须是 def foo(a:) 或 def foo(**opts),不能混用。
  3. block 是语法结构,不是对象:{ ... } 和 do ... end 只是语法,不能直接赋值给变量。要变成对象,用 Proc.new、proc 或 lambda。
  4. lambda 是「真正的匿名方法」:参数检查严格、return 只退出自身;Proc 是「松散的代码片段」,参数缺失补 nil、return 会从外部方法跳出。优先用 lambda,除非你需要 Proc 的宽松行为。
  5. & 的双向转换:方法参数里 &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

卡点急救

  1. wrong number of arguments:Ruby 3 后关键字参数和位置参数分离,检查调用方式和方法签名是否对齐。
  2. LocalJumpError: no block given:方法里写了 yield 但调用时没给 block。加 block_given? 保护,或确保调用带 block。
  3. 方法返回值不对: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 高级技巧。

自测三问

  1. yield 和 block_given? 分别做什么?
  2. 为什么 lambda 比 Proc 更适合做「匿名函数」?
  3. tap 和 then 谁返回原对象、谁返回 block 的返回值?

小结一句话

方法是命名的代码,block 是匿名的代码,yield 是连接两者的桥梁;用 Proc/lambda 把 block 变成可复用的对象,Ruby 的代码表达力就上去了。

延伸

  • Ruby Proc 与 lambda 官方文档
  • 下一课:OOP 与 Mixin——理解 Ruby 的面向对象和模块混入。
加载本节练习…
← 上一课控制流下一课 →OOP 与 Mixin