Thursday, October 23, 2008

(8)异常与异常的处理

程序中会出现异常,或者说错误,比如除法的除数为0,访问一个不存在的文件或者调用一个不存在的方法,又或者访问的网络突然中断,这些都会引起一场,一般情况下异常会造成程序的中断,但是我们也可以通过处理这些异常让程序按照我们的想法继续执行。Ruby中通过raise和rescue还处理异常,raise用于抛出异常,resuce用于捕获异常。所有的异常都继承自Exception类,下列给出了Ruby 中异常类的继承结构,你不必全部记住他们,事实上这样类中没有定义很多方法,你唯一要知道的事下列的一场都属于Ruby中的标准异常,你可以继承或者扩展他们:
Object
 +--Exception
     +--NoMemoryError
     +--ScriptError
     |   +--LoadError
     |   +--NotImplementedError
     |   +--SyntaxError
     +--SecurityError         # Ruby1.8中的标准异常
     +--SignalException
     |   +--Interrupt
     +--SystemExit
     +--SystemStackError      # Ruby1.8中的标准异常
     +--StandardError
         +--ArgumentError
         +--FiberError        # Ruby 1.9中的新增异常
         +--IOError
         |   +--EOFError
         +--IndexError
         |   +--KeyError      # New in 1.9
         |   +--StopIteration # New in 1.9
         +--LocalJumpError
         +--NameError
         |   +--NoMethodError
         +--RangeError
         |   +--FloatDomainError
         +--RegexpError
         +--RuntimeError
         +--SystemCallError
         +--ThreadError
         +--TypeError
         +--ZeroDivisionError
Exception类定义了两个方法用于返回异常信息,message方法用于返回“可读”的异常信息字符串,如果Ruby程序出现了一个未捕获的一场,这个消息会显示给最终用户,通常用于程序员调试代码。另一个方法是backtrace,这个方法返回一个数组,数组中包含了调用的堆栈信息:
filename : linenumber in methodname
以上的代码中filename说明出错的文件位置,linenumer说明那一行出现了错误,methodname说明由那个方法引起的异常。如果你要定义自己的异常类,可以通过set_backtrace来自定义这个信息。
你可以通过扩展当前异常类来定义自己的异常类:
class MyError < StandardError; end

你可以通过raise或者fail来抛出一个异常,有以下几种方式:
  • 如果raise没有参数,那么Ruby会创建一个RuntimeError(不带任何消息)的异常类,并抛出它。
  • 如果raise有一个参数,并且这个参数是一个Exception对象,那么Ruby会直接抛出这个异常对象,通常我们不这样作 。
  • 如果raise有一个参数,并且这个参数是一个字符串,那么Ruby会创建一个RuntimeError对象并将字符串的内容作为异常的消息,我们经常这样使用。
  •  如果raise的第一个参数是一个对象,且这个对象有一个exception方法,那么Ruby会调用这个方法并抛出这个方法返回的异常对象。第二个参数可以是一个字符串,这个字符串会传递给第一个对象的exception方法的第一个参数,作为异常消息。
def factorial(n)                 # 定义一个函数
  raise "bad argument" if n < 1  # 如果条件不满足就抛出异常
  return 1 if n == 1             # factorial(1) 为 1
  n * factorial(n-1)             # 继续计算
end
上面的例子中raise只有一个字符串参数,下面是用其它方式抛出的异常:
raise RuntimeError, "bad argument" if n < 1
raise RuntimeError.new("bad argument") if n < 1
raise RuntimeError.exception("bad argument") if n < 1
就像上面的代码那样,你可以通过new或者exception关键字来创建你自己的异常,这两个方法的字符串参数是异常显示的消息。
Ruby中捕获异常通过rescue关键字,通常一个rescue位于begin和end之间,如下例:
begin
  # 这里放执行代码.
  # 通常他们可以正常的执行 
rescue
  # 这里是rescue子句,如果上面的代码出现异常,那么就跳转到这里,
  # 可以在这里放置其他代码来处理异常 .
end

当然我们可以命名异常对象,如果定义全局变量$!并且require 'English',那么你可以使用$ERROR_INFO 来命名异常,当是一个更好的方法是使用变量:
begin                                # 在这个块中捕获异常
x = factorial(-1)                  # 这里我们故意加入了非法的参数
rescue => ex                         # 将异常对象存储在一个变量ex中
  puts "#{ex.class}: #{ex.message}"  # 捕获这个异常并打印异常消息
end                                  # 结束块
注意这里声明的异常变量不仅仅在异常处理范围,一旦定义了这个变量,你可以在块的外部使用它。
还要注意的是上面这种方式只能捕获StandardError。如果你要捕获StandardError以外的异常,或者你要捕获一个你自定义的异常类型,你必须声明这个异常,或者使用rescue Exception的方式捕获任何类型的异常:


rescue Exception #捕获任何类型的异常
rescue ArgumentError => e #捕获ArgumentError异常
rescue ArgumentError, TypeError => error #将多个异常赋值到一个异常变量中

我们同样可以以一种分支的方式分别处理不同的异常:

rescue Exception #捕获任何类型的异常
rescue ArgumentError => e #捕获ArgumentError异常
rescue ArgumentError, TypeError => error #将多个异常赋值到一个异常变量中
我们前面讲过的retry关键字可以在rescue中使用,这样使用的目的是,有的时候引发的异常只是短暂异常,可能过一会儿就可以正常运行了,比如,当前服务器可能无法访问但是过一会儿就正常了,下面是一个访问网络的例子,它遇到网络异常后尝试再次访问:
require 'open-uri'

tries = 0       # 访问一个URL等待的时间
begin           # 块开始部分
   tries += 1    # 尝试连接 URL并打印连接信息
  open('http://www.example.com/') {|f| puts f.readlines }
rescue OpenURI::HTTPError => e  # 如果得到一个HTTP error
  puts e.message                # 打印异常信息
  if (tries < 4)                # 判断尝试的次数...
    sleep(2**tries)             # 等待 2, 4, 或者 8 秒钟
    retry                       # 然后再试一次!
  end
end
这里还有一个else子句,你可能认为else是在rescue执行失败后执行的语句,其实不然,实际上else子句部分的代码是在当begin部分的代码可以没有异常的正常执行后执行的代码。老实说else在rescue中并不经常使用,如果begin中没有rescue的话使用else是没有意义的。
另外一个重要的关键字是ensure,在这个子句中的代码无论如何都会被执行,比如你要关闭一个文件、一个数据库连接或者提交数据库执行语句。如果ensure在方法中,那么它的值不会作为方法的返回值,也就是说,在你调用一个方法的时候实际上会先执行ensure中的语句然后再执行begin中的语句。不过,如果你在ensure中使用了return关键字那么方法的返回值会被替换为ensure的返回值。
begin
  return 1     # 方法的返回值
ensure
  return 2     # 最终会返回这个值
end
事实上rescue,ensure这些关键字不仅可以用于begin/end之间,还可以用于def定义的模块、类、方法。
下面是resuce子句的一个变体,如果resuce前面的代码出现异常或者raise一个异常,那么resuce后面的代码就会执行:
y = factorial(x) rescue 0
上面的代码等同于:
y = begin
      factorial(x)
    rescue
      0
    end
这种写法的好处是可以省略begin/end,但必须在一行里完成,而且这种变态中只能处理StandardError异常,而不能出来其它类型的异常。

No comments: