原创作者: andyhu1007   阅读:14467次   评论:1条   更新时间:2011-05-26    

在现实世界中,所有程序都会出错。一个优秀的程序可以预期错误的发生,并且优雅地处理它们。

 

一种错误处理的方法是:使用返回码。举个例子,我们在使用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

评论 共 1 条 请登录后发表评论
1 楼 helloqidi 2012-07-27 13:26
关于异常处理的非常好的文章,谢谢!

发表评论

您还没有登录,请您登录后再发表评论

文章信息

Global site tag (gtag.js) - Google Analytics