在现实世界中,所有程序都会出错。一个优秀的程序可以预期错误的发生,并且优雅地处理它们。
一种错误处理的方法是:使用返回码。举个例子,我们在使用open方法打开文件,文件不存在时就会出错。我们可以使用一个特殊的返回码来标识这个错误。
但这种处理方式的问题是:管理这些错误代码会显得非常复杂。比如,我们调用了open,read并最终调用close方法,每一个方法都会返回不同的错误代码,我们需要在调用的外层次使用复杂难懂的代码来管理和区分这些不同的错误代码。
异常机制很好地解决了上述错误处理方法的问题:异常把错误信息打包进一个类中,在抛出一个异常之后,异常会自动在调用栈中‘上浮’,直到遇见声明可以处理相应类型异常的代码。
让我们来看看Ruby中的异常机制有何新鲜之处吧。
Ruby的异常类型层级
Exception
NoMemoryError
ScriptError
LoadError
NotImplementedError
SyntaxError
SignalException
Interrupt
StandardError
ArgumentError
IOError
EOFError
IndexError
LocalJumpError
NameError
NoMethodError
RangeError
FloatDomainError
RegexpError
RuntimeError
SecurityError
SystemCallError
SystemStackError
ThreadError
TypeError
ZeroDivisionError
SystemExit
fatal
当需要抛出一个异常时,可以使用一个内置的异常类,也可以通过继承StandardError类来实现自己的异常类。
异常处理
先看一个简单例子:
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
我们用begin,rescue,end把可能抛出异常的代码以及处理异常的代码包围起来,特别是rescue语句声明了它可以处理的异常类型,rescue之后的代码就是处理异常的代码。
异常对象被抛出之后,存于一个全局变量:$1中。可以看到异常处理代码最后调用了raise,它代表把同一个异常再次抛出。
在一个异常处理块中,可以有多个rescue语句以拥有对不同异常的不同处理,同时一个rescue语句也可以声明捕获多个异常类型。如下面的代码一样:
begin
eval string
rescue SyntaxError, NameError => boom
print "String doesn't compile: " + boom
rescue StandardError => bang
print "Error running script: " + bang
end
到处使用$1不是个好办法,我们可以像上面的代码一样把异常赋予一个变量。
rescue可以不带参数,它的默认参数是StandardError。
begin
eval string
rescue
print "Error running script: " + $1
end
rescue后面不仅仅可以带异常类型参数,也可以是任意的表达式,只要这个表达式返回一个异常类型即可。
这个异常处理是如何找到匹配的异常处理块的呢?其实这个原理跟case语句差不多,它通过$1.kind_of?(parameter)语句来识别和匹配异常处理块。
清理
很多时候,无论程序抛异常与否,我们都得保证在最后做一些清理工作。比如一段从文件读取数据的代码,无论读取是否成功,我们都得保证最后关闭这个文件。我们可以通过在任何的退出点关闭文件来解决这个问题,但这样做非常繁琐,也不能保证照顾到了每个退出点。所以,我们需要一种方法来保证。这就是ensure的作用:
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
ensure
f.close unless f.nil?
end
在上面代码中,可以加入一个else以在没有异常抛出的情况下做一些处理,比如:
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
else
puts "Congratulations-- no errors!"
ensure
f.close unless f.nil?
end
重试
有些时候,异常抛出之后我们希望它可以重试一次或者采用另外一种方式重新尝试。这就是retry的作用:
@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
抛出异常
之前我们都是在处理别人抛出的异常,接下来看看如何抛出我们自己的异常。
raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller
第一种方式:重新抛出当前的异常,或者如果没有当前异常,则抛出一个RuntimeError。
第二种方式:抛出一个以参数为异常信息的RuntimeError。
第三种方式:抛出一个第一个参数类型的异常,以第二个参数为异常的信息,最后一个参数指定堆栈轨迹。可以看到,这种方式在堆栈轨迹中忽略了当前处。我们也可以在堆栈轨迹中去除更多的调用点,比如:
raise ArgumentError, "Name too big", caller[1..-1]
去除了当前、以及当前的调用者两个调用点。
赋予异常更多的信息
有时候,我们需要赋予异常除了message之外的一些信息,以便根据这些信息的不同来处理异常。
比如:
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
Catch和Throw
在有些时候,rescue、raise机制并不能很好地满足需求,比如当我们需要跳出一个很深的嵌套结构时。这时候,就是catch和throw发挥作用的时候。
catch (:done) do
while gets
throw :done unless fields = split(/\t/)
songList.add(Song.new(*fields))
end
songList.play
end
Catch定义了一个block,这个block以一个symbol或者一个字符串为名字。这个block正常执行,直到遇见一个throw。遇见throw之后,程序会一直回朔,直到找到一个匹配symbol的catch,并结束这个catch block。
在上面这个例子中,当遇到throw时,程序会跳出while循环,并且跳过songList.play等其它语句直到catch block的结尾。
throw方法还可以附带一个可选的参数,此参数会作为catch block的返回值。
再来看一个例子:
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
上面代码会处理用户输入,直到遇到一个!符号。
更多请参见:http://www.ruby-doc.org/docs/ProgrammingRuby/html/tut_exceptions.html