强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 动手练习

  1. 实现 my_each
# 不使用 Array#each,实现自己的 each 方法
class Array
  def my_each
    # 你的代码...
  end
end
  1. 实现无限迭代器
# 创建一个无限计数器
# counter.take(5) => [1, 2, 3, 4, 5]
def counter(start = 1, step = 1)
  # 你的代码...
end
  1. 实现管道操作符
# 实现一个管道操作符,将多个函数组合
# 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利用块设计领域特定语言

📖 扩展阅读


上一章← 第 09 章:面向对象编程 下一章第 11 章:元编程 →