Skip to content

Ruby 异常

异常是 Ruby 处理程序执行期间发生的错误或意外事件的方式。如果未处理,异常通常会导致程序终止。适当的异常处理允许程序优雅地响应错误、记录问题、重试操作或采取替代措施。

Ruby 使用 begin、rescue、else、ensure 和 end 关键字来管理异常。

begin
# 可能引发异常的代码(“受保护”块)
# ...
rescue StandardError => variable_for_exception_object
# 处理特定 StandardError 或其子类的代码
# 'variable_for_exception_object' 现在持有异常对象
# ...
rescue AnotherExceptionType => e
# 处理 AnotherExceptionType 的代码
# ...
rescue
# 处理任何继承自 StandardError 的异常
# (如果未指定特定类型,则默认为 StandardError)
# 通常最好捕获(rescue)特定的异常类型。
else
# 仅在 'begin' 块中未引发任何异常时执行的代码
# ...
ensure
# 无论是否发生异常,始终执行的代码。
# 通常用于清理(例如,关闭文件、释放资源)。
# ...
end
  • begin...rescue 块内的代码被执行。
  • 如果发生异常,Ruby 会查找与引发的异常类型(或其超类之一)匹配的 rescue 子句。
  • 如果找到匹配项,则执行该 rescue 块中的代码。可以使用 => e(其中 e 是一个变量)捕获异常对象本身。
  • 如果在 rescue 子句中未指定特定的异常类型(即只有 rescue),则默认捕获 StandardError 及其后代。最佳实践是捕获特定异常或至少 StandardError,而不是 Exception(后者可能会捕获您可能不想干扰的信号,如 Interrupt 或 SystemExit)。
  • else 子句(可选)仅在 begin 块完成且未引发任何异常时执行。
  • ensure 子句(可选)始终执行,无论是否发生异常、异常是否被捕获,或者块是否正常退出。这对于清理任务至关重要。
#!/usr/bin/env ruby
# frozen_string_literal: true
begin
print "输入一个数字: "
num1_str = gets.chomp
num1 = Integer(num1_str) # 对于无效输入,这可能引发 ArgumentError
print "输入另一个数字: "
num2_str = gets.chomp
num2 = Integer(num2_str)
# 如果 num2 为 0,这可能引发 ZeroDivisionError
result = num1 / num2
puts "除法结果: #{result}"
rescue ArgumentError => e
puts "无效输入:请输入有效数字。详情: #{e.message}"
rescue ZeroDivisionError => e
puts "错误:不能除以零。详情: #{e.message}"
rescue StandardError => e
puts "发生了一个意外的标准错误: #{e.message}"
puts "回溯信息: #{e.backtrace.join("\n")}"
else
puts "计算成功,没有错误!"
ensure
puts "正在退出计算块。"
end

如果您为第二个数字输入非数字内容或 0,相应的 rescue 块将处理该错误。

在 rescue 块内部,retry 语句会导致 Ruby 跳回 begin 块的开头并重新执行代码。使用 retry 时需谨慎,以免造成无限循环,通常需要包含计数器或改变条件。

begin
# ... 可能失败的代码 ...
rescue SomeError => e
# ... 处理错误,可能修改状态 ...
retry # 跳到 'begin' 块的开头
end
#!/usr/bin/env ruby
# frozen_string_literal: true
attempts = 0
max_attempts = 3
begin
attempts += 1
puts "尝试 ##{attempts}"
print "输入要打开的文件名: "
filename = gets.chomp
file = File.open(filename, "r")
puts "文件 '#{filename}' 已成功打开。"
puts file.read(50) # Read first 50 chars
file.close
rescue Errno::ENOENT => e # 文件未找到错误
puts "错误: #{e.message}"
if attempts < max_attempts
puts "正在重试..."
retry
else
puts "达到最大尝试次数。无法打开文件。"
end
rescue StandardError => e
puts "发生了另一个错误: #{e.message}"
end

您可以使用 raise 语句显式引发(或抛出)异常。这对于在您自己的代码中发出错误信号非常有用。

# 1. 重新引发当前异常(常用于 rescue 块中)
raise
# 2. 引发带特定消息的 RuntimeError
raise "Error Message"
# 3. 引发带特定消息的特定类型异常
raise ExceptionType, "Error Message"
# 4. 引发特定的异常对象(允许设置自定义属性)
custom_error = MyCustomError.new("Detailed message")
# custom_error.details = { 代码: 123 }
raise custom_error
# 5. 条件引发(不太常见的形式,通常 `raise if condition` 更清晰)
# raise ExceptionType, "Error Message" if condition
#!/usr/bin/env ruby
# frozen_string_literal: true
def check_age(age)
if age < 0
raise ArgumentError, "Age cannot be negative."
elsif age < 18
raise StandardError, "User is underage."
end
puts "年龄 #{age} 是有效的。"
end
begin
check_age(25)
check_age(-5) # 这将引发 ArgumentError
# check_age(16) # 这将引发 StandardError
rescue ArgumentError => e
puts "捕获到 ArgumentError: #{e.message}"
rescue StandardError => e
puts "捕获到 StandardError: #{e.message}"
end
puts "年龄检查块之后。"

输出结果为:

Age 25 is valid.
Caught ArgumentError: Age cannot be negative.
After age check block.

当异常被捕获(例如,rescue => e)时,变量 e 持有异常对象。异常对象的常用方法包括:

  • e.message:错误消息。
  • e.backtrace:表示异常发生时调用堆栈的字符串数组。
  • e.class:异常的类。
#!/usr/bin/env ruby
begin
1 / 0
rescue ZeroDivisionError => e
puts "错误类: #{e.class}"
puts "错误消息: #{e.message}"
puts "回溯信息(前 3 行):"
e.backtrace.first(3).each { |line| puts " #{line}" }
end

$! 全局变量也持有上次引发的异常,$@ 持有其回溯信息,但通常首选使用 => e 语法以提高清晰度和作用域。

catch 和 throw 提供了一种非局部跳转机制,用于在正常处理过程中跳出深度嵌套的结构,主要不是用于错误处理(错误处理是 raise/rescue 的领域)。

catch(:label_name) do ... end 定义一个块。该块内部(或块内调用的任何方法)的 throw(:label_name) 或 throw(:label_name, return_value) 将立即跳到 catch 块的末尾。catch 块然后返回 throw 的第二个参数,如果没有给出则返回 nil。

result = catch(:done) do
# ... 一些处理 ...
10.times do |i|
# ... 更多处理 ...
if i == 5
throw :done, "在迭代 #{i} 处完成" # 跳出并带返回值
end
end
"Completed all iterations" # 如果发生 throw,则不会到达这里
end
puts result # => "Finished at iteration 5"
#!/usr/bin/env ruby
# frozen_string_literal: true
def process_input(prompt_message)
print prompt_message
input = gets.chomp
throw :quit_requested if input.downcase == 'quit'
input
end
puts "输入每个字段的数据。输入 'quit' 提前退出。"
user_data = catch(:quit_requested) do
name = process_input("姓名: ")
age = process_input("年龄: ")
city = process_input("城市: ")
{ name: name, age: age, city: city } # 如果输入全部给出,则返回哈希
end
if user_data.is_a?(Hash)
puts "\n收集到的用户数据:"
user_data.each { |key, value| puts " #{key.capitalize}: #{value}" }
else
puts "\n输入过程提前退出了。"
end

Ruby 中的所有异常类都继承自 Exception 类。然而,通常建议从 StandardError 或其更具体的后代类捕获(rescue),因为 Exception 包括您通常不希望捕获的系统级事件,如 SystemExit(来自 exit)和 Interrupt(来自 Ctrl-C)。

简化的层次结构视图:

Exception
├── NoMemoryError
├── ScriptError
│ ├── LoadError (例如,require 失败)
│ └── SyntaxError
├── SignalException
│ └── Interrupt (Ctrl-C)
├── StandardError (大多数常见的应用程序级错误继承自此)
│ ├── ArgumentError
│ ├── IOError
│ │ └── EOFError
│ ├── IndexError
│ ├── NameError
│ │ └── NoMethodError
│ ├── RangeError
│ ├── RegexpError
│ ├── RuntimeError (raise "message" 的默认类型)
│ ├── TypeError
│ └── ZeroDivisionError
│ └── ... 许多其他 ...
├── SystemExit (来自 exit)
└── ... 其他内部错误 ...

为您的应用程序或库错误定义自定义异常类是良好的实践。这些类通常应继承自 StandardError 或其后代类之一。

#!/usr/bin/env ruby
# frozen_string_literal: true
class NetworkDataError < StandardError
attr_reader :error_code
def initialize(message = "Network data processing error", error_code = nil)
super(message)
@error_code = error_code
end
end
class ApiService
def fetch_data(url)
# 模拟一个可能失败的 API 调用
if url.include?("error_site")
raise NetworkDataError.new("Failed to fetch from #{url}", 503)
elsif url.include?("empty_site")
raise NetworkDataError.new("No data received from #{url}", 204)
end
"Sample data from #{url}"
end
end
service = ApiService.new
urls_to_try = ["http://example.com", "http://error_site.com", "http://empty_site.com"]
urls_to_try.each do |url|
begin
data = service.fetch_data(url)
puts "#{url} 成功:#{data}"
rescue NetworkDataError => e
puts "#{url} 自定义错误:#{e.message}(代码: #{e.error_code || 'N/A'})"
rescue StandardError => e
puts "#{url} 通用错误:#{e.message}"
end
end