异常

Ruby 代码可以抛出异常。

通常,抛出异常是为了提醒正在运行的程序出现了不寻常(即*异常*)的情况,可能需要处理。

Ruby 核心、Ruby 标准库和 Ruby gem 中的代码会在特定情况下生成异常。

File.open('nope.txt') # Raises Errno::ENOENT: "No such file or directory"

抛出的异常

抛出的异常会以某种方式转移程序执行。

未被捕获的异常

如果一个异常没有被*捕获*(请参阅下面的被捕获的异常),则执行会转移到 Ruby 解释器中的代码,该代码会打印消息并退出程序(或线程)。

$ ruby -e "raise"
-e:1:in '<main>': unhandled exception

被捕获的异常

一个*异常处理程序*可以决定在抛出异常时发生什么;处理程序可以*捕获*异常,并防止程序退出。

一个简单的例子

begin
  raise 'Boom!'                # Raises an exception, transfers control.
  puts 'Will not get here.'
rescue
  puts 'Rescued an exception.' # Control transferred to here; program does not exit.
end
puts 'Got here.'

输出

Rescued an exception.
Got here.

一个异常处理程序有几个元素

元素 用途
Begin 子句。 开始处理程序,并包含可能被捕获的任何抛出异常的代码。
一个或多个 rescue 子句。 每个子句都包含“捕获”代码,该代码将针对某些异常执行。
Else 子句(可选)。 包含如果没有抛出异常则要执行的代码。
Ensure 子句(可选)。 包含无论是否抛出异常或是否捕获了异常都要执行的代码。
end 语句。 结束处理程序。’

Begin 子句

begin 子句开始异常处理程序

Rescue 子句

一个 rescue 子句

被捕获的异常

rescue 语句可以包含一个或多个要捕获的类;如果没有给出,则假定为 StandardError

rescue 子句捕获指定的类(或如果未给出则捕获 StandardError)或其任何子类;请参阅 内置异常类层次结构

begin
  1 / 0 # Raises ZeroDivisionError, a subclass of StandardError.
rescue
  puts "Rescued #{$!.class}"
end

输出

Rescued ZeroDivisionError

如果 rescue 语句指定了一个异常类,则仅捕获该类(或其子类之一);此示例以 ZeroDivisionError 退出,该异常未被捕获,因为它不是 ArgumentError 或其子类之一。

begin
  1 / 0
rescue ArgumentError
  puts "Rescued #{$!.class}"
end

rescue 语句可以指定多个类,这意味着其代码捕获任何给定类(或其子类)的异常。

begin
  1 / 0
rescue FloatDomainError, ZeroDivisionError
  puts "Rescued #{$!.class}"
end
多个 Rescue 子句

一个异常处理程序可以包含多个 rescue 子句;在这种情况下,第一个捕获异常的子句会这样做,而之前和之后的子句将被忽略。

begin
  Dir.open('nosuch')
rescue Errno::ENOTDIR
  puts "Rescued #{$!.class}"
rescue Errno::ENOENT
  puts "Rescued #{$!.class}"
end

输出

Rescued Errno::ENOENT
捕获被捕获的异常

rescue 语句可以指定一个变量,其值变为捕获的异常(Exception 或其子类之一的实例)。

begin
  1 / 0
rescue => x
  puts x.class
  puts x.message
end

输出

ZeroDivisionError
divided by 0
全局变量

两个只读全局变量在 rescue 子句中始终具有 nil 值;在那里

示例

begin
  1 / 0
rescue
  p $!
  p $@
end

输出

#<ZeroDivisionError: divided by 0>
["t.rb:2:in 'Integer#/'", "t.rb:2:in '<main>'"]
原因

在 rescue 子句中,方法 Exception#cause 返回 $! 的先前值,该值可能为 nil;在其他地方,该方法返回 nil

示例

begin
  raise('Boom 0')
rescue => x0
  puts "Exception: #{x0.inspect};  $!: #{$!.inspect};  cause: #{x0.cause.inspect}."
  begin
    raise('Boom 1')
  rescue => x1
    puts "Exception: #{x1.inspect};  $!: #{$!.inspect};  cause: #{x1.cause.inspect}."
    begin
      raise('Boom 2')
    rescue => x2
      puts "Exception: #{x2.inspect};  $!: #{$!.inspect};  cause: #{x2.cause.inspect}."
    end
  end
end

输出

Exception: #<RuntimeError: Boom 0>;  $!: #<RuntimeError: Boom 0>;  cause: nil.
Exception: #<RuntimeError: Boom 1>;  $!: #<RuntimeError: Boom 1>;  cause: #<RuntimeError: Boom 0>.
Exception: #<RuntimeError: Boom 2>;  $!: #<RuntimeError: Boom 2>;  cause: #<RuntimeError: Boom 1>.

Else 子句

else 子句

begin
  puts 'Begin.'
rescue
  puts 'Rescued an exception!'
else
  puts 'No exception raised.'
end

输出

Begin.
No exception raised.

Ensure 子句

ensure 子句

def foo(boom: false)
  puts 'Begin.'
  raise 'Boom!' if boom
rescue
  puts 'Rescued an exception!'
else
  puts 'No exception raised.'
ensure
  puts 'Always do this.'
end

foo(boom: true)
foo(boom: false)

输出

Begin.
Rescued an exception!
Always do this.
Begin.
No exception raised.
Always do this.

End 语句

end 语句结束处理程序。

只有在捕获了任何抛出的异常后,才会到达其后面的代码。

无 Begin 的异常处理程序

如上所述,可以使用 beginend 实现异常处理程序。

异常处理程序也可以实现为

重新抛出异常

捕获异常但允许其最终影响可能很有用;例如,程序可以捕获异常,记录有关它的数据,然后“恢复”异常。

这可以通过 raise 方法完成,但以特殊方式完成;一个捕获子句

begin
  1 / 0
rescue ZeroDivisionError
  # Do needful things (like logging).
  raise # Raised exception will be ZeroDivisionError, not RuntimeError.
end

输出

ruby t.rb
t.rb:2:in 'Integer#/': divided by 0 (ZeroDivisionError)
    from t.rb:2:in '<main>'

重试

重试 begin 子句可能很有用;例如,如果它必须访问可能不稳定的资源(例如网页),多次尝试访问可能很有用(希望它可以变为可用)。

retries = 0
begin
  puts "Try ##{retries}."
  raise 'Boom'
rescue
  puts "Rescued retry ##{retries}."
  if (retries += 1) < 3
    puts 'Retrying'
    retry
  else
    puts 'Giving up.'
    raise
  end
end
Try #0.
Rescued retry #0.
Retrying
Try #1.
Rescued retry #1.
Retrying
Try #2.
Rescued retry #2.
Giving up.
# RuntimeError ('Boom') raised.

请注意,retry 会重新执行整个 begin 子句,而不仅仅是失败点之后的部分。

抛出异常

方法 Kernel#raise 抛出一个异常。

自定义异常

要提供其他或替代信息,可以创建自定义异常类。每个类都应该是内置异常类之一的子类(通常是 StandardErrorRuntimeError);请参阅 内置异常类层次结构

class MyException < StandardError; end

消息

每个 Exception 对象都有一个消息,该消息是一个字符串,在创建对象时设置;请参阅 Exception.new

消息无法更改,但可以使用不同的消息创建类似的对象;请参阅 Exception#exception

此方法按定义返回消息

其他两个方法返回消息的增强版本

上述两个方法中的每一个都接受关键字参数 highlight;如果关键字 highlight 的值为 true,则返回的字符串将包含加粗和下划线 ANSI 代码(见下文)以增强消息的外观。

任何异常类(Ruby 或自定义)都可以选择覆盖这些方法中的任何一个,并且可以选择将关键字参数 highlight: true 解释为返回的消息应包含指定颜色、加粗和下划线的 ANSI 代码

由于增强的消息可能会写入非终端设备(例如,写入 HTML 页面),因此最好将 ANSI 代码限制为以下广泛支持的代码。



最好还创建一个方便人类阅读的消息,即使 ANSI 代码以“原样”包含(而不是解释为字体指令)。

回溯

回溯是当前在调用堆栈中的方法的记录;每个这样的方法都已被调用,但尚未返回。

以下方法返回回溯信息

默认情况下,Ruby 将异常的回溯设置为引发异常的位置。

开发者可以通过向 Kernel#raise 提供 backtrace 参数,或使用 Exception#set_backtrace 来调整此设置。

请注意,