异常

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.

异常处理程序有几个组成部分

Element 用法
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
全局变量

两个只读的全局变量始终具有 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 语句结束处理程序。

之后可以达到的代码仅在任何已抛出的异常被捕获时才会被执行。

无 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.

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

请注意: