异常
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 子句开始异常处理程序。
-
可以以
begin语句开始;另请参阅 无 Begin 的异常处理程序。 -
包含将被处理程序覆盖的已抛出异常(如果有)的代码。
-
以第一个后续的
rescue语句结束。
Rescue 子句
一个 rescue 子句
-
以
rescue语句开始。 -
包含将在特定已抛出异常时执行的代码。
-
以第一个后续的
rescue、else、ensure或end语句结束。
已捕获的异常
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 子句
-
以
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 语句结束处理程序。
之后可以达到的代码仅在任何已抛出的异常被捕获时才会被执行。
无 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.
重新抛出异常
捕获异常然后允许其产生最终效果可能很有用;例如,程序可以捕获异常,记录有关它的数据,然后“恢复”该异常。
这可以通过 raise 方法来完成,但以一种特殊的方式;一个捕获子句
-
捕获异常。
-
执行关于异常所需的任何操作(例如记录它)。
-
调用不带参数的
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 抛出异常。
自定义异常
为了提供额外或替代的信息,您可以创建自定义异常类。每个类都应该是内置异常类(通常是 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 代码 红色 \e[31m绿色 \e[32m黄色 \e[33m蓝色 \e[34m品红色 \e[35m青色 \e[36m
-
开始字体属性
Attribute ANSI 代码 粗体 \e[1m下划线 \e[4m
-
结束以上所有
颜色 ANSI 代码 重置 \e[0m
最好也构造一个方便人类阅读的消息,即使 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都将变为重新抛出的位置。