异常¶ ↑
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 子句开始异常处理程序
-
可以以
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
全局变量¶ ↑
两个只读全局变量在 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
子句
-
以
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.
请注意,retry 会重新执行整个 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
-
开始字体属性
属性 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
都将变为重新引发的位置。