Ruby 零基础教程

Ruby 方法与代码块的交互

在 Ruby 中,代码块(Blocks)是可以传递给方法的代码片段。它们是自定义方法行为的一种极其强大的方式。代码块本身并不是对象,但它们可以被转换为对象(即 Proc,我们将在后面的章节中介绍)。它们类似于其他编程语言中的匿名函数或 Lambda 表达式。

在上一章中,我们学习了使用 do...end{} 定义代码块的基础知识。本章我们将深入探讨如何将代码块传递给方法,以及方法内部如何利用这些代码块。

1. 传递代码块的基础

在定义方法时,你不需要显式地声明一个代码块作为参数。相反,如果在调用方法时提供了一个代码块,方法就可以隐式地接收它。方法可以通过使用 block_given? 方法来判断是否接收到了代码块,并可以使用 yield 关键字来执行该代码块。

来看一个简单的例子:

def my_method
  puts "正在执行 my_method"
  if block_given?
    puts "检测到代码块,准备通过 yield 将控制权交给代码块"
    yield
  else
    puts "没有提供代码块"
  end
  puts "准备退出 my_method"
end

my_method 
# 输出: 
# 正在执行 my_method
# 没有提供代码块
# 准备退出 my_method

my_method do
  puts "正在执行代码块内部的逻辑" 
end
# 输出: 
# 正在执行 my_method
# 检测到代码块,准备通过 yield 将控制权交给代码块
# 正在执行代码块内部的逻辑
# 准备退出 my_method

在这个例子中,my_method 使用 block_given? 检查是否提供了代码块。如果是,它就使用 yield 执行该代码块。否则,它会打印 "没有提供代码块"。

代码解析:

  • def my_method: 定义了一个名为 my_method 的方法。
  • block_given?: 这是 Ruby 的一个内置方法。如果调用当前方法时传递了代码块,它返回 true,否则返回 false
  • yield: 这是一个关键字,用于调用传递给该方法的代码块。它就像是把程序的执行控制权暂时转移给了代码块。
  • my_method do ... end: 带着代码块调用 my_methoddo...end 内部的代码就是传入的代码块。

2. 向代码块传递参数

方法不仅可以执行代码块,还可以使用 yield 向代码块传递参数。然后,代码块会将这些参数作为自己的局部变量接收。

def my_method_with_args
  puts "正在执行 my_method_with_args"
  if block_given?
    puts "检测到代码块,准备 yield 并传递参数"
    yield("Hello", "World")
  else
    puts "没有提供代码块"
  end
  puts "准备退出 my_method_with_args"
end

my_method_with_args do |arg1, arg2|
  puts "正在执行代码块,接收到的参数为: #{arg1}, #{arg2}"
end

# 输出:
# 正在执行 my_method_with_args
# 检测到代码块,准备 yield 并传递参数
# 正在执行代码块,接收到的参数为: Hello, World
# 准备退出 my_method_with_args

代码解析:

  • yield("Hello", "World"): 将字符串 "Hello""World" 作为参数传递给代码块。
  • do |arg1, arg2|: 定义了包含两个参数 arg1arg2 的代码块,它们将接收由 yield 传递过来的值。
  • 代码块内部的 arg1 的值变成了 "Hello"arg2 的值变成了 "World"

2.1 代码块参数命名与缺失参数

代码块的参数命名规则与常规方法参数一样。请选择能清晰描述参数含义的名称。

如果 yield 没有传递任何参数,代码块就不应该定义任何参数变量。如果 yield 只传递了一个参数,但代码块却定义了两个参数变量,那么第二个参数的值将会是 nil

def method_with_optional_args
  yield 1
end

method_with_optional_args { |a, b| puts "a: #{a}, b: #{b}" } 
# 输出: a: 1, b: 
# (注:b 的值是 nil,在字符串插值时显示为空白)

3. 常见迭代器中的代码块

许多 Ruby 的内置方法(尤其是迭代器,如 eachmapselectreduce)都在大量使用代码块。

3.1 each 方法

each 方法用于遍历集合(例如数组),并将每个元素 yield 给代码块。

numbers = [1, 2, 3, 4, 5]

numbers.each do |number|
  puts "当前的数字是: #{number}"
end

# 输出:
# 当前的数字是: 1
# 当前的数字是: 2
# 当前的数字是: 3
# 当前的数字是: 4
# 当前的数字是: 5

3.2 map 方法

map 方法根据代码块的返回值转换集合中的每个元素,并返回一个包含转换后元素的新数组。

numbers = [1, 2, 3, 4, 5]

squared_numbers = numbers.map do |number|
  number * number
end

puts squared_numbers.inspect 
# 输出: [1, 4, 9, 16, 25]

3.3 select 方法

select 方法根据代码块的条件过滤集合的元素。它会返回一个新的数组,其中只包含那些让代码块返回 true 的元素。

numbers = [1, 2, 3, 4, 5]

even_numbers = numbers.select do |number|
  number.even?
end

puts even_numbers.inspect 
# 输出: [2, 4]

3.4 reduce (或 inject) 方法

reduce(也称为 inject)方法根据代码块的逻辑,将集合中的所有元素组合成一个单一的值。

numbers = [1, 2, 3, 4, 5]

sum = numbers.reduce(0) do |accumulator, number|
  accumulator + number
end

puts sum 
# 输出: 15

4. 实战案例演示

为了更好地理解代码块的威力,我们来看几个实际应用场景。

4.1 案例 1:自定义日志

让我们创建一个记录日志的方法,它会自动添加时间戳,并使用代码块来动态生成日志信息。

def log_message(log_level)
  timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
  message = yield # 执行代码块以获取日志内容
  puts "[#{timestamp}] [#{log_level}] #{message}"
end

log_message("INFO") { "应用程序启动成功。" }
# 示例输出: [2024-01-01 12:00:00] [INFO] 应用程序启动成功。

log_message("ERROR") { "连接数据库失败。" }
# 示例输出: [2024-01-01 12:00:00] [ERROR] 连接数据库失败。

4.2 案例 2:测量代码执行时间

我们可以创建一个方法,专门用来测算某一段代码块的执行耗时。

require 'benchmark'

def measure_execution_time
  time = Benchmark.realtime do
    yield # 执行传入的代码块并测量时间
  end
  puts "执行耗时: #{time} 秒"
end

measure_execution_time do
  # 模拟一个耗时操作
  sleep(2)
end
# 示例输出: 执行耗时: 2.0012345 秒 (大约值)

4.3 案例 3:资源管理

我们可以创建一个方法来处理文件的打开和关闭。使用代码块可以确保无论发生什么错误,文件最终都会被安全关闭(利用 begin...ensure 结构)。

def with_file(filename, mode)
  file = File.open(filename, mode)
  begin
    yield(file) # 执行代码块,并将文件对象传递进去
  ensure
    file.close  # 确保文件一定会被关闭
  end
end

with_file("my_file.txt", "w") do |file|
  file.write("这是一些写入到文件中的文本。")
end
# 文件 "my_file.txt" 被创建/打开,写入内容后,自动安全关闭。

4.4 案例 4:构建简易 DSL (领域特定语言)

代码块常被用来创建简单的 DSL。假设你想定义一种直观的配置格式:

def configure
  config = {}
  yield config if block_given?
  config
end

my_config = configure do |c|
  c[:name] = "我的应用程序"
  c[:version] = "1.0"
  c[:author] = "John Doe"
end

puts my_config.inspect 
# 输出: {:name=>"我的应用程序", :version=>"1.0", :author=>"John Doe"}