异常

Ruby 代码可以抛出异常。

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

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

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

抛出的异常

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

未捕获的异常

如果异常没有被rescue(参见下文的已捕获的异常),执行将转移到 Ruby 解释器中的代码,该代码会打印消息并退出程序(或线程)。

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

已捕获的异常

异常处理程序可以确定在抛出异常时会发生什么;处理程序可以rescue异常,并可能阻止程序退出。

一个简单的例子

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 的代码(如果有抛出异常)。
一个或多个 rescue 子句。 每个子句都包含“rescue”代码,该代码用于执行特定异常。
Else 子句(可选)。 包含如果未抛出异常则执行的代码。
Ensure 子句(可选)。 包含无论是否抛出异常或是否 rescue 异常都要执行的代码。
<tt>end</tt> 语句。 结束处理程序。`

Begin 子句

begin 子句启动异常处理程序

Rescue 子句

一个 rescue 子句

已捕获的异常

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

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

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

输出

Rescued ZeroDivisionError

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

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

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

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

一个异常处理程序可能包含多个 rescue 子句;在这种情况下,rescue 该异常的第一个子句会执行,并且之前和之后的子句会被忽略。

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

输出

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

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

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

输出

ZeroDivisionError
divided by 0
全局变量

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

示例

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 语句结束处理程序。

只有在任何抛出的异常被 rescue 时才会到达其后的代码。

无 Begin 的异常处理程序

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

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

重新抛出异常

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

这可以通过 raise 方法完成,但以一种特殊的方式;rescue 子句

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.

请注意,重试会重新执行整个 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 来调整此设置。

请注意: