Ruby 入门指南 / 第 10 章:块与迭代器
第 10 章:块与迭代器
“块是 Ruby 最强大的特性之一,它让代码既优雅又灵活。”
10.1 块深入
10.1.1 块的本质
# 块不是对象,但可以被 Proc 和 Lambda 对象化
# 块是方法的隐式参数
# 块的基本形式
[1, 2, 3].each { |n| puts n } # 单行块
[1, 2, 3].each do |n| # 多行块
puts n * 2
end
# yield 调用块
def greet
puts "Hello"
yield if block_given?
puts "Goodbye"
end
greet { puts "Block here!" }
# Hello
# Block here!
# Goodbye
# 多次 yield
def repeat(n)
n.times { yield }
end
repeat(3) { puts "Hello!" }
# Hello!
# Hello!
# Hello!
10.1.2 块参数
# 传递参数给块
def calculate(a, b)
yield(a, b)
end
result = calculate(10, 20) { |x, y| x + y }
puts result # => 30
# 块参数数量灵活
[1, 2, 3].each_with_index do |value, index, extra|
puts "value=#{value}, index=#{index}, extra=#{extra.inspect}"
end
# value=1, index=0, extra=nil
# value=2, index=1, extra=nil
# value=3, index=2, extra=nil
# 块返回值
def find_first(array)
array.each do |item|
return item if yield(item)
end
nil
end
result = find_first([1, 2, 3, 4, 5]) { |n| n > 3 }
puts result # => 4
10.1.3 块变量作用域
# 块捕获定义时的局部变量(闭包)
multiplier = 3
[1, 2, 3].map { |n| n * multiplier } # => [3, 6, 9]
# 块内修改外部变量
total = 0
[1, 2, 3, 4, 5].each { |n| total += n }
puts total # => 15
# Ruby 1.9+ 块参数不会覆盖外部变量
x = 10
[1, 2, 3].each { |x| puts x } # 块内 x 是块参数
puts x # => 10(外部 x 不受影响)
# 块参数阴影(shadowing,Ruby 1.9+ 语法)
x = 10
[1, 2, 3].each { |x| x += 100 } # 这是块参数 x,不是外部 x
puts x # => 10
10.1.4 块与资源管理
# 用块管理资源(确保资源释放)
def with_file(path, mode)
file = File.open(path, mode)
yield(file)
ensure
file&.close
end
with_file("output.txt", "w") do |f|
f.puts "Hello, World!"
end
# 数据库连接管理
def with_connection
conn = establish_connection
yield(conn)
ensure
conn&.close
end
# 计时器
def measure
start = Time.now
yield
elapsed = Time.now - start
puts "Elapsed: #{elapsed.round(4)} seconds"
end
measure { sleep(1) }
10.2 Proc 对象
10.2.1 创建和使用 Proc
# Proc.new
square = Proc.new { |n| n ** 2 }
square.call(5) # => 25
square.(5) # => 25
square[5] # => 25
# proc 方法(等同于 Proc.new)
cube = proc { |n| n ** 3 }
# 从块转换
def make_proc(&block)
block
end
my_proc = make_proc { |n| n * 2 }
my_proc.call(5) # => 10
# 从方法创建
def double(n)
n * 2
end
double_proc = method(:double)
double_proc.call(5) # => 10
10.2.2 Proc 作为闭包
# Proc 捕获创建时的作用域
def make_counter(start = 0)
count = start
increment = -> { count += 1 }
decrement = -> { count -= 1 }
current = -> { count }
{ increment: increment, decrement: decrement, current: current }
end
counter = make_counter(10)
counter[:increment].call # => 11
counter[:increment].call # => 12
counter[:current].call # => 12
counter[:decrement].call # => 11
# 多个 Proc 共享同一作用域
def make_account(balance)
deposit = ->(amount) { balance += amount }
withdraw = ->(amount) { balance -= amount }
get_balance = -> { balance }
{ deposit: deposit, withdraw: withdraw, get_balance: get_balance }
end
account = make_account(1000)
account[:deposit].call(500)
account[:get_balance].call # => 1500
account[:withdraw].call(200)
account[:get_balance].call # => 1300
10.2.3 Proc 与块的转换
# 块 → Proc(使用 &)
def each_pair(hash, &block)
hash.each { |k, v| block.call(k, v) }
end
each_pair({ name: "Alice", age: 25 }) do |k, v|
puts "#{k}: #{v}"
end
# Proc → 块(使用 &)
my_proc = proc { |n| n * 2 }
[1, 2, 3].map(&my_proc) # => [2, 4, 6]
# Symbol → Proc(使用 &)
[1, 2, 3].map(&:to_s) # => ["1", "2", "3"]
# Proc 作为方法参数
def transform(array, transformer)
array.map { |item| transformer.call(item) }
end
upcase = proc { |s| s.upcase }
transform(["hello", "world"], upcase) # => ["HELLO", "WORLD"]
10.3 Lambda 深入
10.3.1 Lambda vs Proc
# Lambda 语法
lam = -> (x) { x * 2 }
lam = -> x { x * 2 }
lam = lambda { |x| x * 2 }
# 关键区别 1:参数检查
lam = -> (a, b) { [a, b] }
prc = proc { |a, b| [a, b] }
lam.call(1, 2) # => [1, 2]
# lam.call(1) # => ArgumentError
prc.call(1) # => [1, nil](Proc 不检查参数数量)
# 关键区别 2:return 行为
def test_proc
p = proc { return "from proc" }
p.call
"after proc" # 不会执行!
end
def test_lambda
l = -> { return "from lambda" }
l.call
"after lambda" # 会执行
end
test_proc # => "from proc"
test_lambda # => "after lambda"
# 关键区别 3:类型
proc { }.class # => Proc
-> { }.class # => Proc
-> { }.lambda? # => true
proc { }.lambda? # => false
10.3.2 Lambda 实用模式
# 条件过滤器
min_age_filter = -> (user) { user[:age] >= 18 }
active_filter = -> (user) { user[:active] }
vip_filter = -> (user) { user[:role] == :vip }
# 组合过滤器
def filter_users(users, *filters)
users.select do |user|
filters.all? { |f| f.call(user) }
end
end
users = [
{ name: "Alice", age: 25, active: true, role: :vip },
{ name: "Bob", age: 17, active: true, role: :user },
{ name: "Charlie", age: 30, active: false, role: :vip }
]
filter_users(users, min_age_filter, active_filter, vip_filter)
# => [{ name: "Alice", age: 25, active: true, role: :vip }]
# 转换管道
def pipeline(*transforms)
-> (value) { transforms.reduce(value) { |v, t| t.call(v) } }
end
process = pipeline(
-> (s) { s.strip },
-> (s) { s.downcase },
-> (s) { s.gsub(/\s+/, "_") }
)
process.call(" Hello World ") # => "hello_world"
10.4 Enumerable 模块
10.4.1 混入 Enumerable
class Collection
include Enumerable
def initialize(*items)
@items = items
end
def each(&block)
@items.each(&block)
end
end
col = Collection.new(3, 1, 4, 1, 5, 9, 2, 6)
col.sort # => [1, 1, 2, 3, 4, 5, 6, 9]
col.select(&:odd?) # => [3, 1, 1, 5, 9]
col.map { |n| n ** 2 } # => [9, 1, 16, 1, 25, 81, 4, 36]
col.reduce(:+) # => 31
col.first(3) # => [3, 1, 4]
col.min # => 1
col.max # => 9
col.sum # => 31
10.4.2 自定义 Enumerable 类
class DateRange
include Enumerable
def initialize(start_date, end_date)
@start = start_date
@end = end_date
end
def each
current = @start
while current <= @end
yield current
current = current.next_day
end
end
end
require "date"
range = DateRange.new(Date.new(2024, 1, 1), Date.new(2024, 1, 5))
range.to_a
# => [#<Date: 2024-01-01>, #<Date: 2024-01-02>, ...]
range.count # => 5
range.select(&:sunday?) # => [](2024-01-01 是周一)
range.map { |d| d.strftime("%A") }
# => ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
10.4.3 高级 Enumerable 方法
# each_with_object
result = [1, 2, 3].each_with_object({}) do |n, hash|
hash[n] = n ** 2
end
# => {1=>1, 2=>4, 3=>9}
# each_cons(滑动窗口)
[1, 2, 3, 4, 5].each_cons(3).to_a
# => [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
# each_slice(分批)
(1..10).each_slice(3).to_a
# => [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
# each_with_index
["a", "b", "c"].each_with_index.to_a
# => [["a", 0], ["b", 1], ["c", 2]]
# group_by
[1, 2, 3, 4, 5, 6].group_by { |n| n.even? }
# => {true=>[2, 4, 6], false=>[1, 3, 5]}
# tally(Ruby 2.7+)
%w[a b a c b a].tally
# => {"a"=>3, "b"=>2, "c"=>1}
# slice_when
[1, 2, 4, 5, 7, 8].slice_when { |a, b| b - a > 1 }.to_a
# => [[1, 2], [4, 5], [7, 8]]
# chunk
[1, 1, 2, 2, 3, 1, 1].chunk { |n| n }.to_a
# => [[1, [1, 1]], [2, [2, 2]], [3, [3]], [1, [1, 1]]]
# chunk_while(Ruby 2.3+)
[1, 2, 4, 5, 7, 8].chunk_while { |a, b| b - a == 1 }.to_a
# => [[1, 2], [4, 5], [7, 8]]
# grep / grep_v
%w[apple banana cherry date].grep(/^b/) # => ["banana"]
[1, 2, 3, 4, 5].grep(2..4) # => [2, 3, 4]
[1, 2, 3, 4, 5].grep_v(2..4) # => [1, 5]
10.5 Enumerator
10.5.1 手动创建 Enumerator
# 使用 Enumerator.new
fibonacci = Enumerator.new do |yielder|
a, b = 0, 1
loop do
yielder << a
a, b = b, a + b
end
end
fibonacci.take(10) # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# 使用 to_enum
enum = [1, 2, 3].to_enum
enum.next # => 1
enum.next # => 2
enum.next # => 3
enum.next # => StopIteration
# 每个方法都可以生成 Enumerator
enum = [1, 2, 3].each
enum = [1, 2, 3].map
enum = [1, 2, 3].select
enum = [1, 2, 3].each_with_index
10.5.2 Enumerator 链
# 链式 Enumerator
enum = [1, 2, 3, 4, 5]
.select { |n| n.odd? }
.map { |n| n ** 2 }
.each
enum.next # => 1
enum.next # => 9
enum.next # => 25
enum.next # => StopIteration
# with_index
["a", "b", "c"].map.with_index { |v, i| "#{i}:#{v}" }
# => ["0:a", "1:b", "2:c"]
# with_object
[1, 2, 3].each.with_object([]) { |n, arr| arr << n * 2 }
# => [2, 4, 6]
10.5.3 Lazy Enumerator
# 惰性求值(处理大数据集)
# 不使用 lazy
(1..Float::INFINITY).select(&:even?).first(5)
# 死循环!
# 使用 lazy
(1..Float::INFINITY).lazy.select(&:even?).first(5)
# => [2, 4, 6, 8, 10]
# 惰性链式操作
result = (1..Float::INFINITY)
.lazy
.select(&:odd?)
.map { |n| n ** 2 }
.select { |n| n > 100 }
.first(5)
# => [121, 169, 225, 289, 361]
# 自定义惰性迭代器
def primes
Enumerator.new do |yielder|
n = 2
loop do
yielder << n if prime?(n)
n += 1
end
end.lazy
end
def prime?(n)
return false if n < 2
(2..Math.sqrt(n).to_i).none? { |i| n % i == 0 }
end
primes.take(10).to_a
# => [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
10.6 块与 DSL
10.6.1 实例执行
# instance_exec - 在块中执行对象的方法
class Config
attr_accessor :host, :port, :debug
def initialize(&block)
instance_exec(&block) if block
end
end
config = Config.new do
self.host = "localhost"
self.port = 3000
self.debug = true
end
puts config.host # => "localhost"
10.6.2 DSL 设计模式
# 路由 DSL(类似 Rails 路由)
class Router
def initialize
@routes = []
end
def get(path, to:)
@routes << { method: :get, path: path, handler: to }
end
def post(path, to:)
@routes << { method: :post, path: path, handler: to }
end
def routes(&block)
instance_exec(&block) if block
@routes
end
end
router = Router.new
routes = router.routes do
get "/", to: "home#index"
get "/users", to: "users#index"
post "/users", to: "users#create"
get "/users/:id", to: "users#show"
end
puts routes
# [{method: :get, path: "/", handler: "home#index"}, ...]
# 测试 DSL(类似 RSpec)
class TestCase
def self.describe(name, &block)
puts "Describe: #{name}"
instance_exec(&block)
end
def self.it(description, &block)
print " it #{description}... "
begin
instance_exec(&block)
puts "✓"
rescue => e
puts "✗ (#{e.message})"
end
end
def self.expect(actual)
Expectation.new(actual)
end
end
class Expectation
def initialize(actual)
@actual = actual
end
def to(matcher)
matcher.match?(@actual)
end
end
class Eq
def initialize(expected)
@expected = expected
end
def match?(actual)
raise "Expected #{@expected}, got #{actual}" unless actual == @expected
true
end
end
def eq(expected)
Eq.new(expected)
end
TestCase.describe "Array" do
it "returns length" do
expect([1, 2, 3].length).to eq(3)
end
it "supports push" do
arr = [1, 2]
arr.push(3)
expect(arr).to eq([1, 2, 3])
end
end
10.7 动手练习
- 实现
my_each
# 不使用 Array#each,实现自己的 each 方法
class Array
def my_each
# 你的代码...
end
end
- 实现无限迭代器
# 创建一个无限计数器
# counter.take(5) => [1, 2, 3, 4, 5]
def counter(start = 1, step = 1)
# 你的代码...
end
- 实现管道操作符
# 实现一个管道操作符,将多个函数组合
# pipe(5, ->(x) { x * 2 }, ->(x) { x + 1 }) => 11
def pipe(value, *functions)
# 你的代码...
end
10.8 本章小结
| 要点 | 说明 |
|---|---|
| 块 | 方法的隐式参数,通过 yield 调用 |
| Proc | 块的对象化,参数宽松,return 退出定义方法 |
| Lambda | 特殊 Proc,参数严格,return 只退出自身 |
| Enumerable | 混入 each 即可获得丰富的迭代方法 |
| Enumerator | 手动控制迭代过程,支持惰性求值 |
| DSL | 利用块设计领域特定语言 |
📖 扩展阅读
- Ruby Enumerable 文档
- Ruby Enumerator 文档
- 《Metaprogramming Ruby 2》— Paolo Perrotta 著
上一章:← 第 09 章:面向对象编程 下一章:第 11 章:元编程 →