第八章 异常,catch和throw
第八章 异常,catch和throw
迄今为止我们已在Pleasantville内开发了代码,这是个没有错误的地方。每个库都可成功调用,用户从不会输入错误数据,资源是丰富和廉价的。当然这并不真实。欢迎回到现实中来。
在现实生活中,会有错误发生。好的程序(和程序员)可预见到错误并优雅地安排对错误的处理。但是这并不总是像说起来这么容易。通常,用于侦测错误的代码并不真的知道它们在做什么。例如,在有些情况下,试图打开一个不存在文件的做法可能是合理的,而这在其它时候可是个致命错误。你的文件处理模块用来做什么?
传统方式是使用返回代码。open方法返回一些特定值来说明它失败了。然后这个值在调用例程层中向外传播,直到有人出于责任而捕获它。
这个方式的问题是管理这么多错误代码可能是件痛苦的事。如果一个函数调用open,然后是read,最后是close,而这每一步都可能返回一个错误代码,这个函数如何能够区别调用返回的这些错误代码。
在很大范围内用异常来解决这个问题。异常让你包装有关的错误信息到一个对象内。然后这个异常对象被自动从调用堆栈向外传播,直到运行时系统找到,明确地声明了它知道如何处理这个异常类型的代码。
Exception 类
包含有关一个异常信息的包是个Exception类或它的子类的对象。Ruby预定义了一个简洁的异常层次,像下一页中表8.1显示的。稍后我们会看到,用这个层次处理异常相当地容易。
当你需要引发一个异常时,你可使用内建的Exception类,或你自己创建的异常类中的一个。如果你自己创建,你可能需要让它成为StandardError或它的子类的一个子类。如果你不这么做,你的异常在缺省情况下不会被捕获。
每个异常都有相关的消息字符串一个堆栈轨迹。如果你定义了自己的异常,你可以添加额外的信息。
处理异常
我们自动点唱机使用一个TCP套接字在互联网上下载歌曲。基本代码很简单(假设filename和socket已经设置完了)。
op_file = File.open(opfile_name, "w")
while data = socket.read(512)
op_file.write(data)
end
如果我们在下载到一半时出现了一个致命的错误,这会怎样?当然,我们不会将一个完整的歌曲存储到歌曲列表中。 “I Did It My *click*.”
让我们添加一些异常处理代码,看看这会有什么帮助。为了对异常进行处理,我们封装引起异常的代码在一个begin/end块中,并且使用一或多个rescue子句来告诉Ruby我们想处理的异常类型。在这个特定的例子中,我们只感兴趣SystemCallError异常(当然也包括它的子类),所以它会出现在rescue行中。在错误处理块内,我们报告错误,关闭和删除输出文件,然后重新引发这个异常。
op_file = File.open(opfile_name, "w")
begin
# Exceptions raised by this code will
# be caught by the following rescue clause
while data = socket.read(512)
op_file.write(data)
end
rescue SystemCallError
$stderr.print "IO failed: " + $!
op_file.close
File.delete(opfile_name)
raise
end
当一个异常出现时,它有单独的异常处理,Ruby放置相关的异常对象的引用在全局变量$!中(异常指针令人惊异地会镜像我们可能引起错误的任何代码)。在前面例子中,我们使用$!变量来格式化我们的错误消息。
在关闭和删除文件后,我们调用无参数的raise,它重新引发$!内的异常。这是个很有用的技术,它允许你编写这样的代码,过滤异常,传递你没有处理的那些异常到上层。它几乎就像是实现的一个对错误处理的继承层次。
你可在一个begin块内使用多个rescue子句,而每个resuce子句可以指定多个要捕获的异常。在每个rescue子句末端时你可以给出一个Ruby的局部变量来接收匹配的异常。很多人认为在下述位置上使用$!更易于阅读。
begin
eval string
rescue SyntaxError, NameError => boom
print "String doesn't compile: " + boom
rescue StandardError => bang
print "Error running script: " + bang
end
Ruby是如何确定哪个rescue子句被执行的?它将它们处理成类似于使用case语句。对begin块内的每个rescue子句,Ruby将被引发异常与每个参数依次比较。如果被引发的异常匹配一个参数,Ruby执行这个rescue子句体并停止查找。这个匹配使用parameter===$!操作完成。对于大多数异常,这意味着如果rescue子句内的异常名若与当前被抛出的异常的类型相同,或是那个异常的一个子类,则匹配将会成功。(注:这种比较这所以会发生,是因为异常是类,类是种模块。而模块内定义的===方法,在做为操作数的类与被调的祖先相同时返回true。)如果你写了无参数rescue子句,则它的缺省参数是StandardError。
如果没有找到匹配的rescue子句,如果一个异常在begin/end块的外部被引发,Ruby在堆栈内移出调用者并从调用者内查找一个异常处理程序,然后是调用者内调用者,依次类推。
尽管rescue子句的参数通常是Exception类的名字,但实际上它们可以是能返回一个异常类的任意表达式(包括方法调用)。
系统错误
但调用操作系统并返回一个错误代码时系统错误被引发。在POSIX系统上,这些错误有如EAGAIN和EPERM等样的名字。(如果你使用Unix系统,你可以输入manerrno来获得这些错误的列表。)
Ruby接受这些错误并将它们包装一个特定的异常对象内。每个对象都是SystemCallError的子类,每个对象都被定义在Errno模块内。这意味着将可使用类名称如Errno::EAGAIN,Errno::EIO,和Errno::EPERM来寻找异常。如果你想获取操作系统的错误代码,每个Errno异常对象都有个类常量叫(有点乱)Errno,它就包含这个值。
Errno::EAGAIN::Errno => 35
Errno::EPERM::Errno => 1
Errno::EIO::Errno => 5
Errno::EWOULDBLOCK::Errno => 35
注意EWOULDBLOCK和EAGAIN有同样的错误号码。这是用于产生个块的计算机操作系统的一个特征—两个常量映射到同一错误号码。要处理它,Ruby会排列它们以便Errno::EAGAIN和Errno::EWOULDBLOCK在rescue子句中被同等对待。你在一个rescue中请求,你就会处理这两者。通过重新定义SystemCallError#===,以便If you ask to rescue one, you’ll rescue either. It does this by redefining SystemCallError#=== so that if two subclasses of SystemCallError are compared, the comparison is done on their error number and not on their position in the hierarchy.
清理
有时候你需要保证代码块的最后部分也会被处理,而不管是否有异常被引发。例如,你可能在进入块时打开一个文件,并且需要确保在块退出时文件会被关闭。
使用ensure子句会做到这点。ensure出现在最后的rescue子句后面,它所包含的代码块在块中止时,总是会得到执行的。它不考虑块是正常退出,还是引发了异常及处理这个异常,或由一个未捕获异常而引起的中止,ensure块都将被执行。
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
ensure
f.close unless f.nil?
end
else子句也是一样的构造,尽管它不是很有用。如果出现的话,它在rescue子句的后面和任何ensure子句的前面。else子句体只有在主代码体没引发异常时才会被执行。
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
else
puts "Congratulationsno errors!"
ensure
f.close unless f.nil?
end
再次恢复执行
有时候你可能要纠正异常的错误。在这些情况下,你可以在一个rescue子句中使用retry语句来重新进入到begin/end块中。明显地,这是个无穷尽的循环,所以使用这个特征要小心。
这个例子的代码会再次审理一个异常,可以查看Minero Aoki的net/smtp.rb库。
@esmtp = true
begin
# First try an extended login. If it fails because the
# server doesn't support it, fall back to a normal login
if @esmtp then
@command.ehlo(helodom)
else
@command.helo(helodom)
end
rescue ProtocolError
if @esmtp then
@esmtp = false
retry
else
raise
end
end
这个代码首先试图使用EHLO命令来连接一个SMTP服务,这个命令没有得到广泛的支持。如果连接失败,代码设置@esmtp变量为false,并且会试图重新连接。如果第二次又失败了,则引发异常给调用者。
主动引发异常
到现在我们一直是在防御,处理由它人引发的异常。你也可反过来。
你可以在你的代码中使用Kernel.raise方法(同义词是Kernel.fail)来引发异常。
raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller
第一种形式只是简单地重新引发当前异常(或者不是当前异常的RuntimeError)。这可以被用在需要在异常被传递前中途截住它的异常处理程序中。
第二种形式创建一个新的RuntimeError异常,用指定字符串设定它的消息。然后在调用堆栈引这个异常。
第三种形式的第一个参数创建一个异常,然后用第二个参数设置相关消息和堆栈轨迹给第三个参数。典型地,第一个参数即可以是异常层次内的类名字(注:技术上,这个参数可以是任何对象,该对象通过返回object.kind_of?(Exception)为true的对象来表示异常消息。),也可以是这些类的一个对象实例的引用。堆栈轨迹通常使用Kernel.caller方法来产生。
这儿是些raise的典型例子。
raise
raise "Missing name" if name.nil?
if i >= names.size
raise IndexError, "#{i} >= size (#{names.size})"
end
raise ArgumentError, "Name too big", caller
在最后例子中,我们从堆栈轨迹中移除了当前例程,它通常在库模块中很有用。我们可使用这个特征。下面代码通过传递调用堆栈的一个子集给新的异常来从堆栈轨迹中移除两例程。
raise ArgumentError, "Name too big", caller[1..1]
给异常添加信息
你可以定义你自己的异常,它持有你需要传递给错误的任何信息。例如,某些类型的网络错误所依赖的环境是短暂的。如果这样一个错误发生,则环境是主要的,你可以在异常中设置个标志来告诉处理者,它是否值得去重新操作。
class RetryException <>
attr :ok_to_retry
def initialize(ok_to_retry)
@ok_to_retry = ok_to_retry
end
end
一个短暂的错误在代码的某处发生。
def read_data(socket)
data = socket.read(512)
if data.nil?
raise RetryException.new(true), "transient read error"
end
# .. normal processing
end
我们调用上一层堆栈来处理异常。
begin
stuff = read_data(socket)
# .. process stuff
rescue RetryException => detail
retry if detail.ok_to_retry
raise
end
Catch 和 Throw
raise和rescue的异常机制中,当出现错误时,它们会放弃异常。有时候这很好,可让你在通常的处理期间从很深的嵌套结构中跳出来。此处的catch和throw也很方便。
catch (:done) do
while line = gets
throw :done unless fields = line.split(/t/)
songlist.add(Song.new(*fields))
end
songlist.play
end
catch定义带有给定名字(可以Symbol或String)标签的块。块通常会被执行直到它遇见throw。
当Ruby遇到一个throw时,它展开调用堆栈来查找带有匹配符号的catch块。当找到时,Ruby在此点上展开堆栈并中止块。所以前面例子中,如果输入中不包含当前被格式化的行,throw将跳到相应的catch的尾部,不只是中止while循环也跳过歌曲列表的播放。如果在调用throw时使用了可选的第二个参数,则catch的值会被做为返回值。
下面例子如果用户对提示的回答是!时,使用throw来中止与用户的会话。
def prompt_and_get(prompt)
print prompt
res = readline.chomp
throw :quit_requested if res == "!"
res
end
catch :quit_requested do
name = prompt_and_get("Name: ")
age = prompt_and_get("Age: ")
sex = prompt_and_get("Sex: ")
# ..
# process information
end
就像这个例子显示的,throw没有必要出现在catch的静态作用域中。

