我们已经开发了一些代码,比较完美的是暂时还没有出现错误。每个库都可以成功调用,用户从不输入无效的数据,并且资源丰富且易获得。但事事无常。欢迎来到真实的世界。
在真实的世界中错误时常发生。好的程序和程序会预计到它们的出现并且合理地处理它们。不过要做到这点并是如同想像的那么简单。通常一段发现错误的代码是没有相应的上下文指导其如何进行下一步的。比如,当尝试打开一个不存在的文件时,有些环境是可以接受的,对另外一些环境却是重大的错误。你的文件处理模块是如何做的呢?
传统方式都是返回错误码。open
方法会返回指定值用于表达当前操作的失败。这个值通过调用过程传递回去,直到某人想对其做出反应为止。
这种处理方式的问题在于管理过多的错误代码会十分痛苦。如果一个函数按顺序分别调用了
open
,read
,close
,每个方法都可能返回错误信息,这个函数的调用者应该如何从返回值中区分这些错误代码?
异常最大程度地解决了这个问题。异常可以让你将错误信息打包成一个对象。异常对象会自动传回调用栈,直到运行时系统发现它,并且能明确这种类型的异常如何处理时为止。
异常类
Exception
类及其子类的对象都可以将异常信息包装起来。Ruby
预定义了一个整洁的异常体系,详情可见在 91 页的表
8.1。如同稍后我们要了解的一样,这个体系让我们更容易考虑对异常的处理。
当你需要抛出异常时,你可以用预定义的 Exception
类,也可以用自己定义的。如果想自定义异常,需要将其作为 SandardError
的子类,或继承 SandardError
的子类。如果不这样做,自定义的异常默认是不会被捕捉的。
每个 Exception
都关联着信息字符串和栈内回溯信息。如果是你自定义的异常,你也可以添加额外信息。
处理异常
我们的点唱机通过 TCP
接口从网络下载歌曲。最简单的代码可以这样写:
opFile = File.open(opName, "w")
while data = socket.read(512)
opFile.write(data)
end
如果在下载中途遇到重大的报错应该怎么办?可以确认的是,我们不希望在歌单中存储未传输完成的歌曲。
让我们添加一些异常处理的代码并看看对我们是否有帮助。我们用 begin/end
将可能会抛出异常的代码包围起来,并用 rescue
语法告诉 Ruby
我们需要处理的异常类型。在这个例子中,我们主要关注
SystemCallError
异常(当然,这也暗含了 SystemCallError
的异常子类),这就是需要通过 rescue
处理的事情。在错误处理的代码块中,我们不仅要报告错误,关闭和删除输出文件,还需要将异常抛出。
opFile = File.open(opName, "w")
begin
# Exceptions raised by this code will
# be caught by the following rescue clause
while data = socket.read(512)
opFile.write(data)
end
rescue SystemCallError
$stderr.print "IO failed: " + $!
opFile.close
File.delete(opName)
raise
end
当异常被抛出时,包括随后处理的异常,Ruby 都会将它们的 Exception
对象引用关联至全局变量 $!
(这个感叹号反应出我们面对代码出现的错误时表示的惊讶)。在之前的例子中,我们通过变量对错误信息进行格式化。
在关闭及删除文件之后,我们直接调用 raise
并且没有传递参数,它会通过 $!
抛出异常。这是一个有用的技巧,它允许你写的代码对异常进行过滤,将当前无法处理的异常抛出至更高层级。不过对于错误流程它也喜欢实现一个继承体系。
在 begin
代码块中你也会有多个 rescue
句式,每个 rescue
都可以指定多个异常抛出。在每个 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
体的代码并停止查找。匹配操作是通过
$!.kind_of?(parameter)
方法实现,因此无论异常与参数是相同的类还是参数是异常的祖先类都将匹配成功。如果 rescue
是无参语句,会默认参数是 StandardError
。
如果没有任何 rescue
语句可以匹配,或异常是在 begin/end
代码块外被引发,Ruby
将移动堆栈并在调用者中寻找异常处理器,如果没有找到将延着调用链一直查找。
尽管 rescue
语句的参数代表 Exception
类的名字,但实际上这些参数可以是任意返回 Exception
类的表达式(也包括方法调用)。
打扫房间
有时你需要保证无论是否有异常被引起,某些流程都在代码块结束后再运行。比如,你可能需要在进入代码时开启文件,并且退出代码块时它会被关闭。
ensure
就可以实现这个功能。ensure
在最后一个 rescue
和代码块调用当前代码块完成之后调用。是否正常退出代码块并不影响它,无论是引发异常并被捕捉,还是被未捕捉的异常停掉,ensure
代码块都将继续运行。
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
ensure
f.close unless f.nil?
end
else
语句也是类似地使用,尽管很少用到。如果使用了
else
,它将会在 rescue
之后,在 ensure
之前运行。else
体中的代码只会在主体代码没有引起异常的情况下执行。
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
else
puts "Congratulations-- no 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'
例子中的代码首先尝试使用 HELO
命令连接 SMTP
服务器,HELO
命令一般情况机器都是支持的。如果连接失败,代码将把 @estmp
变量设置为 false
并且再次尝试连接服务器。如果再次失将会向调用者抛出异常。
抛出异常
我们已经在其他地方抛出的异常处理上了解了许多。也是时候扭转局势主动进攻了。
你可以使用 Kernel::raise
方法抛出异常。
raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller
例子中的第一种形式是简单地将异常抛出(如果抛出时没有将异常作为参数默认是
RuntimeError
)。这种方式通常在传递异常前拦截异常的处理器中使用。
第二种形式会创建新 RuntimeError
异常,通过传递的字符串设置它的信息。并且异常会被抛出到调用栈。
第三种形式会使用第一个参数创建异常,将第二个参数设置为异常信息,并根据堆栈追踪返回抛出给第三个参数。一般情况下第一个参数都是
Exception
体系中一个类的名字,也可以是这些类对象的引用。堆栈追踪一般通过
Kernel::caller
方法产生。
这有一些 raise
实际的例子。
raise
raise "Missing name" if name.nil?
if i >= myNames.size
raise IndexError, "#{i} >= size (#{myNames.size})"
end
raise ArgumentError, "Name too big", caller
在最后的例子中,我们将当前例程从栈回溯中移除了,栈回溯通常在库模块中有用。我们可以更进一步,下面的代码会将两个例程从回溯中移除。
raise ArgumentError, "Name too big", caller[1..-1]
向异常中添加信息
你也可以定义自己的异常,它可以存储你需要从错误端暴露的信息。例如,确切的网络错误类型在环境中转瞬即逝。如果类似错误出现时环境没有问题,你便可以通过异常中的标识告知处理器,它应该再次尝试相同的操作。
class RetryException < RuntimeError
attr :okToRetry
def initialize(okToRetry)
@okToRetry = okToRetry
end
end
可能有某处代码如下一样,一个转瞬即逝的错误发生了。
def readData(socket)
data = socket.read(512)
if data.nil?
raise RetryException.new(true), "transient read error"
end
# .. normal processing
end
在调用栈的更高层级中我们将对异常进行处理。
begin
stuff = readData(socket)
# .. process stuff
rescue RetryException => detail
retry if detail.okToRetry
raise
end
捕捉和抛出
当程序运行出错时通过 raise
和 rescue
机制放弃执行是种良好的方式,它可以很好地从深层次嵌套结构从跳出。这也是
catch
和 throw
迟早用到的地方。
catch (:done) do
while gets
throw :done unless fields = split(/\t/)
songList.add(Song.new(*fields))
end
songList.play
end
catch
通过预设的名字定义了一个代码块(也许是一个 Symbol
也可以是一个字符串)。除非有 throw
的发生,否则这个代码块将正常运行。
当 Ruby 运行至 throw
时,它将把调用栈压缩,以寻找有相同匹配符号的 catch
代码块。当寻找到后,Ruby
会将调用栈解压至目标点并暂停代码块。如果调用 throw
时选填了第二个参数,此参数将作为 catch
的值返回。因此,在之前的例子中如果输入不包含指定格式的内容,throw
将跳转至相同的 catch
结尾,不止是结束掉 while
循环同时也会跳过歌单的播放。
接下来的例子中,如果用户输入「!」我们将通过 throw
停止与用户的互动。
def promptAndGet(prompt)
print prompt
res = readline.chomp
throw :quitRequested if res == "!"
return res
end
catch :quitRequested do
name = promptAndGet("Name: ")
age = promptAndGet("Age: ")
sex = promptAndGet("Sex: ")
# ..
# process information
end
如同这个例子表明的一样,throw
不一定需要出现在 catch
的静态域中。
本文翻译自《Programming Ruby》,主要目的是自己学习使用,文中翻译不到位之处烦请指正,如需转载请注明出处
网友评论