Rescue
异常处理的基本语法可归纳如下:
begin
# Some code which may cause an exception
rescue <Exception Class>
# Code to recover from the exception
end
下面是一个处理尝试除以零的异常的程序示例:
begin
x = 1/0
rescue Exception
x = 0
puts( $!.class )
puts( $! )
end
运行此代码时,除以零的尝试会导致异常。如果未处理(如示例程序 div_by_zero.rb),程序将崩溃。但是,通过将有问题的代码放在异常处理块(begin
和 end
之间)中,我已经能够在以 rescue
开头的部分中捕获异常。我做的第一件事是将变量 x
设置为有意义的值。接下来是这两个令人费解的语句:
puts( $!.class )
puts( $! )
在 Ruby 中,$!
是一个全局变量,为其分配了最后一个捕获的异常对象。打印 $!.class
会显示类名,这里是 “ZeroDivisionError”;单独打印变量 $!
会显示异常对象中包含的错误信息,这里是 “divided by 0”。
我一般都不太热衷于依赖全局变量,特别是当它们的’名字’与 $!
一样不具有描述性时。幸运的是,还有另一种选择。你可以通过将’关联运算符’(assoc operator),=>
放在异常的类名之后和变量名之前,将变量名与异常对象相关联:
rescue Exception => exc
你现在可以使用变量名称(此处为 exc
)来引用 Exception 对象:
puts( exc.class )
puts( exc )
Exceptions 有一个家族树(家谱)…
要理解rescue
子句如何捕获异常,只要记住,在 Ruby 中异常是对象,并且像所有其它对象一样,它们由一个类定义。此外,还有一个明确的“继承链”,就像所有 Ruby 对象都继承自 Object 类一样。虽然看起来很明显,当你除以零时,你将得到一个 ZeroDivisionError 异常,在现实世界的代码中,有时候异常的类型不是那么可预测的。例如,假设你有一个基于用户提供的两个值进行除法计算的方法:
def calc( val1, val2 )
return val1 / val2
end
这可能会产生各种不同的异常。显然,如果用户输入的第二个值为 0,我们将得到 ZeroDivisionError。
但是,如果第二个值是字符串(string),则异常将是 TypeError,而第一个值是字符串时,它将是 NoMethodError(因为 String 类没有定义’除法运算符’ /
)。这里的 rescue
块处理所有可能发生的异常:
def calc( val1, val2 )
begin
result = val1 / val2
rescue Exception => e
puts( e.class )
puts( e )
result = nil
end
return result
end
通常,针对不同的异常采取不同的行为会很有用。你可以通过添加多个 rescue
块来实现。每个 rescue
子句都可以处理多个异常类型,异常类名用逗号分隔。这里我的 calc
方法在一个子句中处理 TypeError 和 NoMethodError 异常,并使用 catch-all 异常处理程序来处理其它所有异常类型:
def calc( val1, val2 )
begin
result = val1 / val2
rescue TypeError, NoMethodError => e
puts( e.class )
puts( e )
puts( "One of the values is not a number!" )
result = nil
rescue Exception => e
puts( e.class )
puts( e )
result = nil
end
return result
end
Object 类是所有异常类(exceptions)的最终祖先类。
从 Object 类开始,派生出子类 Exception,然后是 StandardError,最后是更具体的异常类型,例如 ZeroDivisionError。如果你愿意,你可以编写一个rescue
子句来处理 Object 类,因为 Object 是所有对象的祖先,这样确实会成功匹配一个异常对象: # This is possible… rescue Object => exc但是,尽可能匹配 Exception 类的相关后代类通常更有用。作为更好的措施,附加一个处理 StandardError 或 Exception 对象的 rescue
子句是很有用的,以防止你没考虑到的异常类型被漏掉。你可以运行 exception_tree.rb
程序来查看 ZeroDivisionError 异常的家族树(继承链)。在处理多个异常类型时,应始终让 rescue
子句先处理特定类型的异常,然后使用 rescue
子句处理通用类型的异常。
当特定类型异常(例如 TypeError)处理完时,begin..end
异常块将会退出,因此执行流程不会“进入”通用类型的 rescue
子句。但是,如果 rescue
子句首先处理通用类型的异常,那么它将处理所有类型的异常,因此任何用来处理更具体的类型的异常子句都将永远不会执行。
例如,如果我在 calc
方法中颠倒了 rescue
子句的顺序,首先放置了通用的 Exception 处理程序,这将匹配所有的异常类型,因此特定的 TypeError 和 NoMethodError 异常处理子句永远都不会运行:
# This is incorrect...
rescue Exception => e
puts( e.class )
puts( e )
result = nil
rescue TypeError, NoMethodError => e
puts( e.class )
puts( e )
puts( "Oops! This message will never be displayed!" )
result = nil
end