Ruby 零基础教程

Ruby 代码块 (Blocks)

代码块(Blocks)是 Ruby 中极其核心的基础概念。它们是可以传递给方法的代码片段,为你提供了强大而灵活的编程能力。通过代码块,你可以在不修改方法本身的情况下,自定义方法的行为。

理解代码块是掌握 Ruby 表达力并充分利用其特性的关键。本章将探索定义代码块的两种方式:使用 do...end 和使用 {}。我们将讨论它们的语法、优先级差异以及各自的适用场景。这些知识将为你后续学习如何在方法中使用代码块,以及深入探讨 Procs 和 Lambdas 等高阶概念打下坚实的基础。

1. 理解代码块:do...end 与 {}

在 Ruby 中,代码块是一段可以附加到方法调用上的代码。你可以把它想象成一个没有名字的方法,可以作为参数传递。代码块本身并不是对象,但它们可以被转换为对象(即 Procs 和 Lambdas,我们将在后续章节介绍)。代码块最大的优势在于,它允许你向方法中注入自定义的逻辑。

Ruby 中有两种定义代码块的方式:

  1. 使用 do...end 关键字
  2. 使用花括号 {}

让我们通过示例来逐一探索。

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]

在这里,由于 {} 的高优先级,两个代码块分别正确地紧密绑定到了对应的 mapselect 方法上。如果这里换成 do...end,代码极有可能会报错,或者需要用括号强行指定执行顺序。