Ruby Study 10 : Exception Handling

简介:
exception 在程序开发中是不可或缺的部分, 程序在使用过程中总会遇到不可预知的问题, 例如可预知的访问文件的程序可能在访问过程中文件被删除或移动等. 但是总会有不可预知的, 不可能在写程序时全部规避掉.
所以就有了exception handling.
在Ruby中exception 是Exception calss或者它的subclass的object. 下面来详细的讲解一下异常时如何捕获exception.
注意Ruby的Exception Handling分为几个层面 , 异常执行, 正常执行, 确保执行, 重试, 延迟处理等.
1. rescue, Execute code when error occurs
基本语法 :
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
puts( x )

执行结果 :
ZeroDivisionError
divided by 0
0

1/0 . 1是Fixnum接收者,  / 是Fixnum的public instance method. 0是传入的message. 
1除以0 当然是不允许的. 所以rescue Exception, 指捕获最大范围的异常对象. 
$! 是一个全局变量, 存储最后一次异常的object.  这里显然是 ZeroDivisionError 的object.
divided by 0 是ZeroDivisionError对象输出的消息. 可以写一个to_s返回其他消息.
例如 :
class ZeroDivisionError   def to_s     return "you cann't divided by 0"   end end  begin    x = 1/0 rescue Exception    x = 0    puts( $!.class )    puts( $! ) end puts( x )

执行结果 :
ZeroDivisionError you cann't divided by 0 0

$! 这个全局变量, 如果不想使用的话, 也可以用一个 assoc operator 也就是=> 赋予捕获到的Exception object给其他的变量.
例如 :
def calc( val1, val2 )
    return val1 / val2
end

begin
  result = calc(10,0)
rescue Exception => e1
  puts(e1.class)
  puts(e1)
  result = 0
end
puts(result)

执行结果 :
ZeroDivisionError divided by 0 0

接下来我们来看看Exception的继承关系 :
def putclass(obj1)   p(obj1.superclass)   if obj1.superclass != nil then putclass(obj1.superclass) end end  putclass(ZeroDivisionError)

输出结果 :
StandardError Exception Object BasicObject nil

接下来看看另几种错误,NoMethodError 和 TypeError.
例如 :
def putclass(obj1)
  p(obj1.superclass)
  if obj1.superclass != nil then putclass(obj1.superclass) end
end

def calc( val1, val2 )
    return val1 / val2
end

begin
  result = calc("a",0)
rescue Exception => e1
  puts(e1.class)
  puts(e1)
  result = 0
end
puts(result)

putclass(e1.class)

begin
  result = calc(10,"a")
rescue Exception => e1
  puts(e1.class)
  puts(e1)
  result = 0
end
puts(result)

putclass(e1.class)

输出结果 :
NoMethodError undefined method `/' for "a":String 0 NameError StandardError Exception Object BasicObject nil TypeError String can't be coerced into Fixnum 0 StandardError Exception Object BasicObject nil

在一个块(begin end)中可以写多个rescue捕获, 当捕获到异常匹配后则不再进行下一个rescue.
所以要把范围小的写在前面, 范围大的异常写在后面. 例如 :
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  p calc("a",1)

执行结果 :
NoMethodError undefined method `/' for "a":String One of the values is not a number! nil

如果把上面的两个rescue倒过来写的话, 后面的rescue就不可能被执行到.因为Exception包含了TypeError和NoMethodError.

2. ensure, Execute code whether or not an error occurs
ensure表示不管有没有异常都必须执行的代码.
例如打开了一个文件, 不管有没有异常都要关闭它. 又或者最后要回到程序执行时的目录.
startdir = Dir.getwd
begin
   Dir.chdir( "X:\\" )  # 假设这个目录不存在或为空
   puts( `dir` )
rescue Exception => e
   puts e.class
   puts e
ensure
   Dir.chdir( startdir )
end

p startdir
p Dir.getwd

执行结果 :
Errno::ENOENT
No such file or directory - X:\
"C:/Users/digoal/Desktop"
"C:/Users/digoal/Desktop"

再来测试一个,
# cat test.rb
#!/opt/ruby/bin/ruby
f = File.new( "text" )
begin
    for i in (1..6) do
        puts("line number: #{f.lineno}")
        line = f.gets.chomp
        num = line.to_i
        puts( "Line '#{line}' is converted to #{num}" )
        puts( 100 / num )
    end
rescue Exception => e
    puts( e.class )
    puts( e )
ensure
    f.close
    puts( "File closed" )
end
# text文件的最后一行是字符串, 无法转换为Fixnum, 因此会捕获到异常, 
# 不管有没有异常我们都需要关闭这个文件. 因此ensure代码中写了f.close
# cat text
1
2
3
4
5
jd

执行结果 :
line number: 0
Line '1' is converted to 1
100
line number: 1
Line '2' is converted to 2
50
line number: 2
Line '3' is converted to 3
33
line number: 3
Line '4' is converted to 4
25
line number: 4
Line '5' is converted to 5
20
line number: 5
Line 'jd' is converted to 0
ZeroDivisionError
divided by 0
File closed

这里用到了File的几个public instance method, lineno, gets. 实际上这两个方法是来自IO的.
File是IO的subclass.

3. else, Execute code when no error occurs
else 指当begin后面的代码正常运行的情况下, 则执行else中的代码, 格式 :
begin
        # code which may cause an exception   
rescue [Exception Type]
else    # optional section executes if no exception occurs
ensure  # optional exception always executes
end

例如 : 
def doCalc( aNum )
   begin
      result = 100 / aNum.to_i
   rescue Exception => e     # executes when there is an error
      result = 0
      msg = "Error: " + e.to_s
   else                      # executes when there is no error
      msg = "Result = #{result}"
   ensure                    # always executes
      msg = "You entered '#{aNum}'. " + msg
   end
   return msg
end

p doCalc("a")
p doCalc(1)

执行结果 :
"You entered 'a'. Error: divided by 0"
"You entered '1'. Result = 100"

4. Error numbers
Ruby的Exception子类很多, 也分得比较细.
有一个比较特别的Module是Errno.  Errno包含很多常量, 这些常量的值可以通过Errno::常量名::Errno来获取. 
同时这些常量又是Exception的下下级子类. 例如我们来看一个例子 :
def showFamily( aClass )
    if (aClass != nil) then    
        puts( "#{aClass} :: about to recurse with aClass.superclass = #{aClass.superclass.inspect}" )
        showFamily( aClass.superclass )
    end
end

def chDisk( aChar )
    startdir = Dir.getwd
    begin
        Dir.chdir( "#{aChar}:\\" )
        puts( `dir` )
    rescue Exception => e
        showFamily( e.class ) # to see ancestors, uncomment 
        puts e.class           # ...and comment out this
        puts e
        ensure
        Dir.chdir( startdir )
    end    
end
chDisk( "F" )  # F是我的光驱, 没放光盘,
chDisk( "X" )  # X是一个不存在的盘,
chDisk( "ABC" )  # ABC则是一个格式错误的盘符.

执行结果 : 
Errno::EACCES :: about to recurse with aClass.superclass = SystemCallError
SystemCallError :: about to recurse with aClass.superclass = StandardError
StandardError :: about to recurse with aClass.superclass = Exception
Exception :: about to recurse with aClass.superclass = Object
Object :: about to recurse with aClass.superclass = BasicObject
BasicObject :: about to recurse with aClass.superclass = nil
Errno::EACCES
Permission denied - F:\
Errno::ENOENT :: about to recurse with aClass.superclass = SystemCallError
SystemCallError :: about to recurse with aClass.superclass = StandardError
StandardError :: about to recurse with aClass.superclass = Exception
Exception :: about to recurse with aClass.superclass = Object
Object :: about to recurse with aClass.superclass = BasicObject
BasicObject :: about to recurse with aClass.superclass = nil
Errno::ENOENT
No such file or directory - X:\
Errno::EINVAL :: about to recurse with aClass.superclass = SystemCallError
SystemCallError :: about to recurse with aClass.superclass = StandardError
StandardError :: about to recurse with aClass.superclass = Exception
Exception :: about to recurse with aClass.superclass = Object
Object :: about to recurse with aClass.superclass = BasicObject
BasicObject :: about to recurse with aClass.superclass = nil
Errno::EINVAL
Invalid argument - ABC:\

Errno API中的介绍 : 

Ruby exception objects are subclasses of Exception. However, operating systems typically report errors using plain integers. Module Errno is created dynamically to map these operating system errors to Ruby classes, with each error number generating its own subclass of SystemCallError. As the subclass is created in module Errno, its name will start Errno::.

The names of the Errno:: classes depend on the environment in which Ruby runs. On a typical Unix or Windows platform, there are Errno classes such as Errno::EACCES, Errno::EAGAIN, Errno::EINTR, and so on.

The integer operating system error number corresponding to a particular error is available as the class constantErrno::error::Errno.

   Errno::EACCES::Errno   #=> 13    Errno::EAGAIN::Errno   #=> 11    Errno::EINTR::Errno    #=> 4 

The full list of operating system errors on your particular platform are available as the constants of Errno.

   Errno.constants   #=> :E2BIG, :EACCES, :EADDRINUSE, :EADDRNOTAVAIL, ...
p Errno.constants.sort
[:E2BIG, :EACCES, :EADDRINUSE, :EADDRNOTAVAIL, :EADV, :EAFNOSUPPORT, :EAGAIN, :EALREADY, :EAUTH, :EBADE, :EBADF, :EBADFD, :EBADMSG, :EBADR, :EBADRPC, :EBADRQC, :EBADSLT, :EBFONT, :EBUSY, :ECANCELED, :ECHILD, :ECHRNG, :ECOMM, :ECONNABORTED, :ECONNREFUSED, :ECONNRESET, :EDEADLK, :EDEADLOCK, :EDESTADDRREQ, :EDOM, :EDOOFUS, :EDOTDOT, :EDQUOT, :EEXIST, :EFAULT, :EFBIG, :EFTYPE, :EHOSTDOWN, :EHOSTUNREACH, :EIDRM, :EILSEQ, :EINPROGRESS, :EINTR, :EINVAL, :EIO, :EIPSEC, :EISCONN, :EISDIR, :EISNAM, :EKEYEXPIRED, :EKEYREJECTED, :EKEYREVOKED, :EL2HLT, :EL2NSYNC, :EL3HLT, :EL3RST, :ELIBACC, :ELIBBAD, :ELIBEXEC, :ELIBMAX, :ELIBSCN, :ELNRNG, :ELOOP, :EMEDIUMTYPE, :EMFILE, :EMLINK, :EMSGSIZE, :EMULTIHOP, :ENAMETOOLONG, :ENAVAIL, :ENEEDAUTH, :ENETDOWN, :ENETRESET, :ENETUNREACH, :ENFILE, :ENOANO, :ENOATTR, :ENOBUFS, :ENOCSI, :ENODATA, :ENODEV, :ENOENT, :ENOEXEC, :ENOKEY, :ENOLCK, :ENOLINK, :ENOMEDIUM, :ENOMEM, :ENOMSG, :ENONET, :ENOPKG, :ENOPROTOOPT, :ENOSPC, :ENOSR, :ENOSTR, :ENOSYS, :ENOTBLK, :ENOTCONN, :ENOTDIR, :ENOTEMPTY, :ENOTNAM, :ENOTRECOVERABLE, :ENOTSOCK, :ENOTSUP, :ENOTTY, :ENOTUNIQ, :ENXIO, :EOPNOTSUPP, :EOVERFLOW, :EOWNERDEAD, :EPERM, :EPFNOSUPPORT, :EPIPE, :EPROCLIM, :EPROCUNAVAIL, :EPROGMISMATCH, :EPROGUNAVAIL, :EPROTO, :EPROTONOSUPPORT, :EPROTOTYPE, :ERANGE, :EREMCHG, :EREMOTE, :EREMOTEIO, :ERESTART, :ERFKILL, :EROFS, :ERPCMISMATCH, :ESHUTDOWN, :ESOCKTNOSUPPORT, :ESPIPE, :ESRCH, :ESRMNT, :ESTALE, :ESTRPIPE, :ETIME, :ETIMEDOUT, :ETOOMANYREFS, :ETXTBSY, :EUCLEAN, :EUNATCH, :EUSERS, :EWOULDBLOCK, :EXDEV, :EXFULL, :NOERROR]

查询Errno常量值 : 
for err in Errno.constants.sort do 
   errnum = eval( "Errno::#{err}::Errno" )
   print( "#{err} #{errnum} ," ) 
end
结果
E2BIG 7 ,EACCES 13 ,EADDRINUSE 10048 ,EADDRNOTAVAIL 10049 ,EADV 0 ,EAFNOSUPPORT 10047 ,EAGAIN 11 ,EALREADY 10037 ,EAUTH 0 ,EBADE 0 ,EBADF 9 ,EBADFD 0 ,EBADMSG 0 ,EBADR 0 ,EBADRPC 0 ,EBADRQC 0 ,EBADSLT 0 ,EBFONT 0 ,EBUSY 16 ,ECANCELED 0 ,ECHILD 10 ,ECHRNG 0 ,ECOMM 0 ,ECONNABORTED 10053 ,ECONNREFUSED 10061 ,ECONNRESET 10054 ,EDEADLK 36 ,EDEADLOCK 36 ,EDESTADDRREQ 10039 ,EDOM 33 ,EDOOFUS 0 ,EDOTDOT 0 ,EDQUOT 10069 ,EEXIST 17 ,EFAULT 14 ,EFBIG 27 ,EFTYPE 0 ,EHOSTDOWN 10064 ,EHOSTUNREACH 10065 ,EIDRM 0 ,EILSEQ 42 ,EINPROGRESS 10036 ,EINTR 4 ,EINVAL 22 ,EIO 5 ,EIPSEC 0 ,EISCONN 10056 ,EISDIR 21 ,EISNAM 0 ,EKEYEXPIRED 0 ,EKEYREJECTED 0 ,EKEYREVOKED 0 ,EL2HLT 0 ,EL2NSYNC 0 ,EL3HLT 0 ,EL3RST 0 ,ELIBACC 0 ,ELIBBAD 0 ,ELIBEXEC 0 ,ELIBMAX 0 ,ELIBSCN 0 ,ELNRNG 0 ,ELOOP 10062 ,EMEDIUMTYPE 0 ,EMFILE 24 ,EMLINK 31 ,EMSGSIZE 10040 ,EMULTIHOP 0 ,ENAMETOOLONG 38 ,ENAVAIL 0 ,ENEEDAUTH 0 ,ENETDOWN 10050 ,ENETRESET 10052 ,ENETUNREACH 10051 ,ENFILE 23 ,ENOANO 0 ,ENOATTR 0 ,ENOBUFS 10055 ,ENOCSI 0 ,ENODATA 0 ,ENODEV 19 ,ENOENT 2 ,ENOEXEC 8 ,ENOKEY 0 ,ENOLCK 39 ,ENOLINK 0 ,ENOMEDIUM 0 ,ENOMEM 12 ,ENOMSG 0 ,ENONET 0 ,ENOPKG 0 ,ENOPROTOOPT 10042 ,ENOSPC 28 ,ENOSR 0 ,ENOSTR 0 ,ENOSYS 40 ,ENOTBLK 0 ,ENOTCONN 10057 ,ENOTDIR 20 ,ENOTEMPTY 41 ,ENOTNAM 0 ,ENOTRECOVERABLE 0 ,ENOTSOCK 10038 ,ENOTSUP 0 ,ENOTTY 25 ,ENOTUNIQ 0 ,ENXIO 6 ,EOPNOTSUPP 10045 ,EOVERFLOW 0 ,EOWNERDEAD 0 ,EPERM 1 ,EPFNOSUPPORT 10046 ,EPIPE 32 ,EPROCLIM 10067 ,EPROCUNAVAIL 0 ,EPROGMISMATCH 0 ,EPROGUNAVAIL 0 ,EPROTO 0 ,EPROTONOSUPPORT 10043 ,EPROTOTYPE 10041 ,ERANGE 34 ,EREMCHG 0 ,EREMOTE 10071 ,EREMOTEIO 0 ,ERESTART 0 ,ERFKILL 0 ,EROFS 30 ,ERPCMISMATCH 0 ,ESHUTDOWN 10058 ,ESOCKTNOSUPPORT 10044 ,ESPIPE 29 ,ESRCH 3 ,ESRMNT 0 ,ESTALE 10070 ,ESTRPIPE 0 ,ETIME 0 ,ETIMEDOUT 10060 ,ETOOMANYREFS 10059 ,ETXTBSY 0 ,EUCLEAN 0 ,EUNATCH 0 ,EUSERS 10068 ,EWOULDBLOCK 10035 ,EXDEV 18 ,EXFULL 0 ,NOERROR 0 ,


5. retry, Attempt to Execute code again after an error
如果想在捕获到异常后重试begin后面的代码, 可以在rescue中写上retry. 例如 :
#!/opt/ruby/bin/ruby
def doCalc
    begin
        print( "Enter a number: " )
        aNum = gets().chomp()
        result = 100 / aNum.to_i
    rescue Exception => e
        result = 0
        puts( "Error: " + e.to_s + "\nPlease try again." )    # Ruby 1.9如果要在字符连接中使用Exception的返回值, 需要使用to_s方法.
        retry           # retry on exception
    else
        msg = "Result = #{result}"
    ensure
        msg = "You entered '#{aNum}'. " + msg
    end
    return msg
end
doCalc

执行结果 : 
./test.rb 
Enter a number: 0
Error: divided by 0
Please try again.
Enter a number: "1"
Error: divided by 0
Please try again.
Enter a number: a
Error: divided by 0
Please try again.
Enter a number: 10

为了防止死循环, 可以加一个限制重试的次数.
#!/opt/ruby/bin/ruby
def doCalc
    tries = 0
    begin
        tries += 1
        print( "Enter a number: " )
        aNum = gets().chomp()
        result = 100 / aNum.to_i   
    rescue Exception => e      
        msg = "Error: " + e.to_s
        puts( msg )
        puts( "tries = #{tries}" )
        result = 0      
        if tries < 3 then # set a fixed number of retries
           retry 
        end         
    else
        msg = "Result = #{result}"
    ensure
        msg = "You entered '#{aNum}'. " + msg
    end
    return msg
end
doCalc

执行结果 :
./test.rb 
Enter a number: a
Error: divided by 0
tries = 1
Enter a number: b
Error: divided by 0
tries = 2
Enter a number: c
Error: divided by 0
tries = 3


6. raise, Reactivate a Handled Error
最后要说的是raise, raise是 Kernel的一个public instance method.
详细的说明 : 
raise raise(string) raise(exception [, string [, array]]) fail fail(string) fail(exception [, string [, array]])   click to toggle source

With no arguments, raises the exception in $! or raises a RuntimeError if $! is nil. With a single Stringargument, raises a RuntimeError with the string as a message. Otherwise, the first parameter should be the name of an Exception class (or an object that returns an Exception object when sent an exception message). The optional second parameter sets the message associated with the exception, and the third parameter is an array of callback information. Exceptions are caught by the rescue clause of begin...end blocks.

   raise "Failed to create socket"    raise ArgumentError, "No parameters", caller
例子1 :
begin
    raise ZeroDivisionError.new( "I'm afraid you divided by Zero" )
rescue Exception => e
    puts( e.class )
    puts( "message: " + e.to_s )
end

执行结果 : 
ZeroDivisionError
message: I'm afraid you divided by Zero

例子2 :
class NoNameError < Exception
    def to_str
        "No Name given!"
    end
end
def sayHello( aName )
    begin
        if (aName == "") or (aName == nil) then
             raise NoNameError 
        end
    rescue Exception => e
        puts( e.class )
        puts( "error message: " + e.to_s )
        puts( e.backtrace )
    else
        puts( "Hello #{aName}" )
    end
end
sayHello(nil)

执行结果 : 
NoNameError
error message: NoNameError
C:/Users/digoal/Desktop/new.rb:9:in `sayHello'
C:/Users/digoal/Desktop/new.rb:19:in `<main>'

这里的backtrace是Exception的public instance method.
详细解释 : 
backtrace → array   click to toggle source

Returns any backtrace associated with the exception. The backtrace is an array of strings, each containing either ``filename:lineNo: in `method’’’ or ``filename:lineNo.’‘

   def a      raise "boom"    end     def b      a()    end     begin      b()    rescue => detail      print detail.backtrace.join("\n")    end

produces:
  prog.rb:2:in `a'    prog.rb:6:in `b'    prog.rb:10

7. omitting begin and end
使用异常捕获时, 在方法,Class以及Module里面可以不写begin和end.
例如 :
def calc
        result = 1/0
    rescue Exception => e
        puts( e.class )
        puts( e )
        result = nil
    return result
end

class X
        @@x = 1/0
    rescue Exception => e
        puts( e.class )
        puts( e )
end

module Y
        @@x = 1/0
    rescue Exception => e
        puts( e.class )
        puts( e )
end

最后不要混淆了catch,throw和异常捕获.
Ruby中catch和throw的用法类似别的编程语言的goto. 而不是异常捕获

【参考】
The Book Of Ruby
Ruby 1.9.3 API

 
         
目录
相关文章
|
移动开发 关系型数据库 Ruby