异常¶ ↑
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 子句启动异常处理程序
-
可能以
begin
语句开始;另请参见无 Begin 的异常处理程序。 -
包含其抛出的异常(如果有)被处理程序覆盖的代码。
-
以第一个后续的
rescue
语句结束。
Rescue 子句¶ ↑
一个 rescue 子句
-
以
rescue
语句开始。 -
包含将要针对特定抛出的异常执行的代码。
-
以第一个后续的
rescue
、else
、ensure
或end
语句结束。
已捕获的异常¶ ↑
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 子句中;在那里
-
$!
:包含被 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
子句
-
以
else
语句开始。 -
包含如果 begin 子句中没有抛出异常则要执行的代码。
-
以第一个后续的
ensure
或end
语句结束。
begin puts 'Begin.' rescue puts 'Rescued an exception!' else puts 'No exception raised.' end
输出
Begin. No exception raised.
Ensure 子句¶ ↑
ensure 子句
-
以
ensure
语句开始。 -
包含无论是否抛出异常,以及是否处理了抛出的异常都要执行的代码。
-
以第一个后续的
end
语句结束。
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 的异常处理程序¶ ↑
如上所述,可以使用 begin
和 end
实现异常处理程序。
也可以将异常处理程序实现为
-
方法体
def foo(boom: false) # Serves as beginning of exception handler. puts 'Begin.' raise 'Boom!' if boom rescue puts 'Rescued an exception!' else puts 'No exception raised.' end # Serves as end of exception handler.
-
代码块
Dir.chdir('.') do |dir| # Serves as beginning of exception handler. raise 'Boom!' rescue puts 'Rescued an exception!' end # Serves as end of exception handler.
重新抛出异常¶ ↑
rescue 异常,但允许其最终效果可能很有用;例如,程序可以 rescue 异常,记录有关它的数据,然后“恢复”异常。
这可以通过 raise
方法完成,但以一种特殊的方式;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
抛出异常。
自定义异常¶ ↑
要提供其他或替代信息,您可以创建自定义异常类。每个类都应该是内置异常类之一(通常是 StandardError
或 RuntimeError
)的子类;请参阅内置异常类层次结构。
class MyException < StandardError; end
消息¶ ↑
每个 Exception
对象都有一个消息,这是一个在创建对象时设置的字符串;请参阅Exception.new
。
消息无法更改,但您可以创建具有不同消息的类似对象;请参阅Exception#exception
。
此方法返回定义的消息
另外两个方法返回消息的增强版本
-
Exception#detailed_message
:添加异常类名称,以及可选的高亮显示。 -
Exception#full_message
:添加异常类名称和回溯,以及可选的高亮显示。
上述两个方法都接受关键字参数 highlight
;如果关键字 highlight
的值为 true
,则返回的字符串包含粗体和下划线 ANSI 代码(见下文),以增强消息的外观。
任何异常类(Ruby 或自定义)都可以选择重写这些方法中的任何一个,并且可以选择将关键字参数 highlight: true
解释为返回的消息应包含指定颜色、粗体和下划线的 ANSI 代码。
因为增强的消息可能会写入非终端设备(例如,HTML 页面),所以最好将 ANSI 代码限制为这些广泛支持的代码。
-
开始字体颜色
颜色 ANSI 代码 红色 <tt>\\e[31m</tt> 绿色 <tt>\\e[32m</tt> 黄色 <tt>\\e[33m</tt> 蓝色 <tt>\\e[34m</tt> 品红色 <tt>\\e[35m</tt> 青色 <tt>\\e[36m</tt>
-
开始字体属性
属性 ANSI 代码 粗体 <tt>\\e[1m</tt> 下划线 <tt>\\e[4m</tt>
-
结束以上所有
颜色 ANSI 代码 重置 <tt>\\e[0m</tt>
最好创建一个方便人类阅读的消息,即使 ANSI 代码以“原样”包含(而不是解释为字体指令)。
回溯¶ ↑
回溯是当前在调用堆栈中的方法的记录;每个这样的方法都已被调用,但尚未返回。
这些方法返回回溯信息
-
Exception#backtrace
: 返回调用堆栈的字符串数组,如果调用堆栈为空则返回nil
。 -
Exception#backtrace_locations
: 返回调用堆栈的Thread::Backtrace::Location
对象数组,如果调用堆栈为空则返回nil
。每个Thread::Backtrace::Location
对象提供有关被调用方法的详细信息。
默认情况下,Ruby 将异常的调用堆栈设置为引发异常的位置。
开发者可以通过向 Kernel#raise
提供 backtrace
参数,或者使用 Exception#set_backtrace
来调整此设置。
请注意:
-
默认情况下,
backtrace
和backtrace_locations
都表示相同的调用堆栈; -
如果开发者通过上述方法之一将调用堆栈设置为
Thread::Backtrace::Location
的数组,它们仍然表示相同的调用堆栈; -
如果开发者将调用堆栈设置为字符串或字符串数组
-
通过
Kernel#raise
:backtrace_locations
将变为nil
; -
通过
Exception#set_backtrace
:backtrace_locations
将保留原始值; -
如果开发者通过
Exception#set_backtrace
将调用堆栈设置为nil
,则backtrace_locations
将保留原始值;但如果随后重新引发异常,则backtrace
和backtrace_locations
都将变为重新引发异常的位置。