Ruby 代码块 (Blocks)
代码块(Blocks)是 Ruby 中极其核心的基础概念。它们是可以传递给方法的代码片段,为你提供了强大而灵活的编程能力。通过代码块,你可以在不修改方法本身的情况下,自定义方法的行为。
理解代码块是掌握 Ruby 表达力并充分利用其特性的关键。本章将探索定义代码块的两种方式:使用 do...end 和使用 {}。我们将讨论它们的语法、优先级差异以及各自的适用场景。这些知识将为你后续学习如何在方法中使用代码块,以及深入探讨 Procs 和 Lambdas 等高阶概念打下坚实的基础。
1. 理解代码块:do...end 与 {}
在 Ruby 中,代码块是一段可以附加到方法调用上的代码。你可以把它想象成一个没有名字的方法,可以作为参数传递。代码块本身并不是对象,但它们可以被转换为对象(即 Procs 和 Lambdas,我们将在后续章节介绍)。代码块最大的优势在于,它允许你向方法中注入自定义的逻辑。
Ruby 中有两种定义代码块的方式:
- 使用
do...end关键字 - 使用花括号
{}
让我们通过示例来逐一探索。
2. do...end 代码块
do...end 结构通常用于定义多行代码块。它以 do 关键字开始,以 end 关键字结束。当关联的方法调用该代码块时,do...end 内部的代码就会被执行。
# 示例:配合 each 方法使用 do...end
numbers = [1, 2, 3, 4, 5]
numbers.each do |number|
puts "当前数字是: #{number}"
puts "该数字的平方是: #{number * number}"
end
puts "---"
# 另一个包含条件判断的示例
numbers.each do |number|
puts "正在处理 #{number}"
puts "#{number} 是偶数" if number.even?
end在这个例子中,numbers.each 是一个遍历 numbers 数组中每个元素的方法。do...end 代码块定义了要对每个元素执行的具体操作。|number| 这部分用于将当前的元素作为参数传递给代码块内部。
3. {} 代码块
花括号 {} 通常用于定义单行代码块。虽然它也可以用于多行,但在多行场景下,为了代码的可读性,通常更推荐使用 do...end。相比 do...end,{} 的写法更加简洁。
# 示例:配合 each 方法使用 {}
numbers = [1, 2, 3, 4, 5]
numbers.each { |number| puts "当前数字是: #{number}" }
puts "---"
# 另一个在代码块中包含多个语句的示例(使用分号或换行)
numbers.each { |number|
puts "正在处理 #{number}"
puts "#{number} 是偶数" if number.even?
}这个例子与前一个类似,只是使用了花括号 {} 替代了 do...end。请注意,当在 {} 中编写多条语句时,为了代码整洁,强烈建议将其改写为 do...end 多行形式。
4. 代码块参数 (Block Parameters)
无论是 do...end 还是 {},都可以接收参数。参数定义在代码块开头的两根竖线 | | 之间。这些参数的数量和含义是由调用该代码块的方法来决定的。
# 示例:包含多个参数的代码块
hash = { a: 1, b: 2, c: 3 }
hash.each do |key, value|
puts "键: #{key}, 值: #{value}"
end
puts "---"
hash.each { |key, value| puts "键: #{key}, 值: #{value * 2}" } # 单行 {}在这个例子中,hash 对象的 each 方法会向代码块传递两个参数:哈希中每一对键值对的 key(键)和 value(值)。
5. 核心差异:优先级 (Precedence)
do...end 和 {} 之间最主要的区别在于它们的优先级。
花括号 {} 的优先级高于 do...end。这意味着 Ruby 会尝试将 {} 代码块与它在这一行看到的最靠近的方法紧密结合起来。如果不加注意,这可能会导致意想不到的结果。相反,do...end 的优先级较低,在处理方法链或复杂表达式时,它的意图通常更清晰。
# 示例:优先级差异演示
def some_method(arg)
puts "方法被调用,参数为: #{arg.inspect}"
yield if block_given? # 我们稍后会讲 yield,它的作用是执行代码块。
return arg
end
# 使用 do...end
result = some_method([1, 2, 3]) do |x|
puts "代码块被执行"
end
puts "---"
# 使用 {} - 请注意这里的区别!
result = some_method([1, 2, 3]) { |x| puts "代码块被执行" }
puts "---"
# 另一个经典的优先级陷阱示例
def another_method
return 1, 2, 3
end
p another_method.map { |x| x * 2 } # 正确 - 代码块被精确传递给了 map 方法
p another_method.map do |x| x * 2 end # 结果不符预期 - do...end 优先级低,它会被传递给最外层的 p 方法,而不是 map在最后一个例子中,由于 {} 优先级高,它会紧紧“抱住”左边最近的 map 方法。而 do...end 优先级低,它会越过 map,尝试把自己绑定到整行最外层的 p 方法上,从而导致程序的行为与我们的预期不符。
6. 何时使用哪种代码块?
以下是在 do...end 和 {} 之间做选择的通用准则:
do...end:用于多行代码块,尤其是代码块内包含复杂逻辑时。此外,当你进行方法链式调用,或者调用带有常规参数的方法并希望代码块清晰地关联到该方法时,请使用它。{}:用于单行代码块以保持代码简洁。当你明确需要高优先级时(例如,将代码块作为参数传递给链式调用中特定的方法),也请使用它。
6.1 总结对比表
| 特性 | do...end | {} |
|---|---|---|
| 语法 | do ... end | { ... } |
| 多行适用性 | 推荐用于多行代码块 | 可以使用,但多行时可读性较差 |
| 单行适用性 | 可以使用,但不够简洁 | 推荐用于单行代码块 |
| 优先级 | 较低 (结合较松散) | 较高 (结合较紧密) |
| 可读性 | 适合复杂逻辑,结构清晰 | 适合简单逻辑,代码紧凑 |
7. 实用案例与演示
让我们来看一些在实际开发中使用这两种代码块的例子。
7.1 过滤数组 (select 方法)
# 使用 do...end
numbers = [1, 2, 3, 4, 5, 6]
even_numbers = numbers.select do |number|
number.even?
end
puts "偶数: #{even_numbers}" # 输出: 偶数: [2, 4, 6]
puts "---"
# 使用 {}
odd_numbers = numbers.select { |number| number.odd? }
puts "奇数: #{odd_numbers}" # 输出: 奇数: [1, 3, 5]在这里,我们使用 select 方法来过滤数字数组。代码块定义了过滤的条件(保留返回 true 的元素)。
7.2 计算平方和
# 使用 do...end
numbers = [1, 2, 3, 4, 5]
sum_of_squares = 0
numbers.each do |number|
sum_of_squares += number * number
end
puts "平方和: #{sum_of_squares}" # 输出: 平方和: 55
puts "---"
# 使用 {} (配合 inject 方法更加简洁)
numbers = [1, 2, 3, 4, 5]
sum_of_squares = numbers.inject(0) { |sum, number| sum + number * number }
puts "平方和: #{sum_of_squares}" # 输出: 平方和: 55在这个例子中,我们计算了数组元素的平方和。{} 版本的代码展示了如何使用高级的 inject 方法配合单行代码块来实现极其紧凑的代码。
7.3 遍历 Hash (哈希)
# 使用 do...end
person = { name: "Alice", age: 30, city: "New York" }
person.each do |key, value|
puts "键: #{key}, 值: #{value}"
end
puts "---"
# 使用 {}
person.each { |key, value| puts "#{key.capitalize}: #{value}" }7.4 方法链与优先级实战
def modify_array(array)
array.map { |x| x * 2 }.select { |x| x > 5 }
end
numbers = [1, 2, 3, 4, 5]
modified_numbers = modify_array(numbers)
puts modified_numbers.inspect # 输出: [6, 8, 10]在这里,由于 {} 的高优先级,两个代码块分别正确地紧密绑定到了对应的 map 和 select 方法上。如果这里换成 do...end,代码极有可能会报错,或者需要用括号强行指定执行顺序。