Rescue

异常处理的基本语法可归纳如下:

  1. begin
  2. # Some code which may cause an exception
  3. rescue <Exception Class>
  4. # Code to recover from the exception
  5. end

下面是一个处理尝试除以零的异常的程序示例:

exception1.rb
  1. begin
  2. x = 1/0
  3. rescue Exception
  4. x = 0
  5. puts( $!.class )
  6. puts( $! )
  7. end
div_by_zero.rb

运行此代码时,除以零的尝试会导致异常。如果未处理(如示例程序 div_by_zero.rb),程序将崩溃。但是,通过将有问题的代码放在异常处理块(beginend 之间)中,我已经能够在以 rescue 开头的部分中捕获异常。我做的第一件事是将变量 x 设置为有意义的值。接下来是这两个令人费解的语句:

  1. puts( $!.class )
  2. puts( $! )

在 Ruby 中,$! 是一个全局变量,为其分配了最后一个捕获的异常对象。打印 $!.class 会显示类名,这里是 “ZeroDivisionError”;单独打印变量 $! 会显示异常对象中包含的错误信息,这里是 “divided by 0”。

我一般都不太热衷于依赖全局变量,特别是当它们的’名字’与 $! 一样不具有描述性时。幸运的是,还有另一种选择。你可以通过将’关联运算符’(assoc operator),=> 放在异常的类名之后和变量名之前,将变量名与异常对象相关联:

exception2.rb
  1. rescue Exception => exc

你现在可以使用变量名称(此处为 exc)来引用 Exception 对象:

  1. puts( exc.class )
  2. puts( exc )
exception_tree.rb

Exceptions 有一个家族树(家谱)…

要理解 rescue 子句如何捕获异常,只要记住,在 Ruby 中异常是对象,并且像所有其它对象一样,它们由一个类定义。此外,还有一个明确的“继承链”,就像所有 Ruby 对象都继承自 Object 类一样。

虽然看起来很明显,当你除以零时,你将得到一个 ZeroDivisionError 异常,在现实世界的代码中,有时候异常的类型不是那么可预测的。例如,假设你有一个基于用户提供的两个值进行除法计算的方法:

  1. def calc( val1, val2 )
  2. return val1 / val2
  3. end

这可能会产生各种不同的异常。显然,如果用户输入的第二个值为 0,我们将得到 ZeroDivisionError。

但是,如果第二个值是字符串(string),则异常将是 TypeError,而第一个值是字符串时,它将是 NoMethodError(因为 String 类没有定义’除法运算符’ /)。这里的 rescue 块处理所有可能发生的异常:

multi_except.rb
  1. def calc( val1, val2 )
  2. begin
  3. result = val1 / val2
  4. rescue Exception => e
  5. puts( e.class )
  6. puts( e )
  7. result = nil
  8. end
  9. return result
  10. end

通常,针对不同的异常采取不同的行为会很有用。你可以通过添加多个 rescue 块来实现。每个 rescue 子句都可以处理多个异常类型,异常类名用逗号分隔。这里我的 calc 方法在一个子句中处理 TypeError 和 NoMethodError 异常,并使用 catch-all 异常处理程序来处理其它所有异常类型:

multi_except2.rb
  1. def calc( val1, val2 )
  2. begin
  3. result = val1 / val2
  4. rescue TypeError, NoMethodError => e
  5. puts( e.class )
  6. puts( e )
  7. puts( "One of the values is not a number!" )
  8. result = nil
  9. rescue Exception => e
  10. puts( e.class )
  11. puts( e )
  12. result = nil
  13. end
  14. return result
  15. end
exception_tree.rb

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 异常处理子句永远都不会运行:

multi_except_err.rb
  1. # This is incorrect...
  2. rescue Exception => e
  3. puts( e.class )
  4. puts( e )
  5. result = nil
  6. rescue TypeError, NoMethodError => e
  7. puts( e.class )
  8. puts( e )
  9. puts( "Oops! This message will never be displayed!" )
  10. result = nil
  11. end