Exceptions, Catch, and Throw

我们已经开发了一些代码,比较完美的是暂时还没有出现错误。每个库都可以成功调用,用户从不输入无效的数据,并且资源丰富且易获得。但事事无常。欢迎来到真实的世界。

在真实的世界中错误时常发生。好的程序和程序会预计到它们的出现并且合理地处理它们。不过要做到这点并是如同想像的那么简单。通常一段发现错误的代码是没有相应的上下文指导其如何进行下一步的。比如,当尝试打开一个不存在的文件时,有些环境是可以接受的,对另外一些环境却是重大的错误。你的文件处理模块是如何做的呢?

传统方式都是返回错误码。open
方法会返回指定值用于表达当前操作的失败。这个值通过调用过程传递回去,直到某人想对其做出反应为止。

这种处理方式的问题在于管理过多的错误代码会十分痛苦。如果一个函数按顺序分别调用了
openreadclose,每个方法都可能返回错误信息,这个函数的调用者应该如何从返回值中区分这些错误代码?

异常最大程度地解决了这个问题。异常可以让你将错误信息打包成一个对象。异常对象会自动传回调用栈,直到运行时系统发现它,并且能明确这种类型的异常如何处理时为止。

异常类

Exception 类及其子类的对象都可以将异常信息包装起来。Ruby
预定义了一个整洁的异常体系,详情可见在 91 页的表
8.1。如同稍后我们要了解的一样,这个体系让我们更容易考虑对异常的处理。

当你需要抛出异常时,你可以用预定义的 Exception 类,也可以用自己定义的。如果想自定义异常,需要将其作为 SandardError 的子类,或继承 SandardError 的子类。如果不这样做,自定义的异常默认是不会被捕捉的。

每个 Exception
都关联着信息字符串和栈内回溯信息。如果是你自定义的异常,你也可以添加额外信息。

处理异常

我们的点唱机通过 TCP
接口从网络下载歌曲。最简单的代码可以这样写:

opFile = File.open(opName, "w")
while data = socket.read(512)
  opFile.write(data)
end

如果在下载中途遇到重大的报错应该怎么办?可以确认的是,我们不希望在歌单中存储未传输完成的歌曲。

让我们添加一些异常处理的代码并看看对我们是否有帮助。我们用 begin/end
将可能会抛出异常的代码包围起来,并用 rescue 语法告诉 Ruby
我们需要处理的异常类型。在这个例子中,我们主要关注
SystemCallError 异常(当然,这也暗含了 SystemCallError
的异常子类),这就是需要通过 rescue
处理的事情。在错误处理的代码块中,我们不仅要报告错误,关闭和删除输出文件,还需要将异常抛出。

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

当异常被抛出时,包括随后处理的异常,Ruby 都会将它们的 Exception
对象引用关联至全局变量 $!
(这个感叹号反应出我们面对代码出现的错误时表示的惊讶)。在之前的例子中,我们通过变量对错误信息进行格式化。

在关闭及删除文件之后,我们直接调用 raise
并且没有传递参数,它会通过 $!
抛出异常。这是一个有用的技巧,它允许你写的代码对异常进行过滤,将当前无法处理的异常抛出至更高层级。不过对于错误流程它也喜欢实现一个继承体系。

begin 代码块中你也会有多个 rescue 句式,每个 rescue
都可以指定多个异常抛出。在每个 rescue 语句的结尾你都会给
Ruby 一个局部变量名以接收匹配的异常。许多人发现这样比 $!
全局替换的方式可读性更高。

begin
  eval string
rescue SyntaxError, NameError => boom
  print "String doesn't compile: " + boom
rescue StandardError => bang
  print "Error running script: " + bang
end"'"

Ruby 是如何决定哪个 rescue 语句应该执行的?它的执行方式和
case 声明十分相似。对 begin 代码块中的每个 rescue 语句而言,Ruby
依次将引发的异常与每个参数进行比较。如果引发的异常与参数匹配,Ruby
将执行 rescue 体的代码并停止查找。匹配操作是通过
$!.kind_of?(parameter)
方法实现,因此无论异常与参数是相同的类还是参数是异常的祖先类都将匹配成功。如果 rescue 是无参语句,会默认参数是 StandardError

如果没有任何 rescue 语句可以匹配,或异常是在 begin/end
代码块外被引发,Ruby
将移动堆栈并在调用者中寻找异常处理器,如果没有找到将延着调用链一直查找。

尽管 rescue 语句的参数代表 Exception
类的名字,但实际上这些参数可以是任意返回 Exception
类的表达式(也包括方法调用)。

打扫房间

有时你需要保证无论是否有异常被引起,某些流程都在代码块结束后再运行。比如,你可能需要在进入代码时开启文件,并且退出代码块时它会被关闭。

ensure 就可以实现这个功能。ensure 在最后一个 rescue
和代码块调用当前代码块完成之后调用。是否正常退出代码块并不影响它,无论是引发异常并被捕捉,还是被未捕捉的异常停掉,ensure
代码块都将继续运行。

f = File.open("testfile")
begin
  # .. process
rescue
  # .. handle error
ensure
  f.close unless f.nil?
end

else 语句也是类似地使用,尽管很少用到。如果使用了
else,它将会在 rescue 之后,在 ensure
之前运行。else
体中的代码只会在主体代码没有引起异常的情况下执行。

f = File.open("testfile")
begin
  # .. process
rescue
  # .. handle error
else
  puts "Congratulations-- no errors!"
ensure
  f.close unless f.nil?
end

再做一次

有时你也许需要确认异常的原因。在这些例子中,你能够在 rescue 中使用
retry 声明重复整个 begin/end
代码块。我们清楚的是,当前这是一块巨大区域的无限循环,所以使用这个功能时需要当心(要将手指轻置于停止键)。

作为重试异常的样例,可以看看接下来这段改编至 Minero Aoki net/smtp.rb
库的代码。

@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'

例子中的代码首先尝试使用 HELO 命令连接 SMTP
服务器,HELO
命令一般情况机器都是支持的。如果连接失败,代码将把 @estmp
变量设置为 false
并且再次尝试连接服务器。如果再次失将会向调用者抛出异常。

抛出异常

我们已经在其他地方抛出的异常处理上了解了许多。也是时候扭转局势主动进攻了。

你可以使用 Kernel::raise 方法抛出异常。

raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller

例子中的第一种形式是简单地将异常抛出(如果抛出时没有将异常作为参数默认是
RuntimeError)。这种方式通常在传递异常前拦截异常的处理器中使用。

第二种形式会创建新 RuntimeError
异常,通过传递的字符串设置它的信息。并且异常会被抛出到调用栈。

第三种形式会使用第一个参数创建异常,将第二个参数设置为异常信息,并根据堆栈追踪返回抛出给第三个参数。一般情况下第一个参数都是
Exception
体系中一个类的名字,也可以是这些类对象的引用。堆栈追踪一般通过
Kernel::caller 方法产生。

这有一些 raise 实际的例子。

raise

raise "Missing name" if name.nil?

if i >= myNames.size
  raise IndexError, "#{i} >= size (#{myNames.size})"
end

raise ArgumentError, "Name too big", caller

在最后的例子中,我们将当前例程从栈回溯中移除了,栈回溯通常在库模块中有用。我们可以更进一步,下面的代码会将两个例程从回溯中移除。

raise ArgumentError, "Name too big", caller[1..-1]

向异常中添加信息

你也可以定义自己的异常,它可以存储你需要从错误端暴露的信息。例如,确切的网络错误类型在环境中转瞬即逝。如果类似错误出现时环境没有问题,你便可以通过异常中的标识告知处理器,它应该再次尝试相同的操作。

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

捕捉和抛出

当程序运行出错时通过 raiserescue
机制放弃执行是种良好的方式,它可以很好地从深层次嵌套结构从跳出。这也是
catchthrow 迟早用到的地方。

catch (:done)  do
  while gets
    throw :done unless fields = split(/\t/)
    songList.add(Song.new(*fields))
  end
  songList.play
end

catch 通过预设的名字定义了一个代码块(也许是一个 Symbol
也可以是一个字符串)。除非有 throw
的发生,否则这个代码块将正常运行。

当 Ruby 运行至 throw
时,它将把调用栈压缩,以寻找有相同匹配符号的 catch
代码块。当寻找到后,Ruby
会将调用栈解压至目标点并暂停代码块。如果调用 throw
时选填了第二个参数,此参数将作为 catch
的值返回。因此,在之前的例子中如果输入不包含指定格式的内容,throw
将跳转至相同的 catch 结尾,不止是结束掉 while
循环同时也会跳过歌单的播放。

接下来的例子中,如果用户输入「!」我们将通过 throw
停止与用户的互动。

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

如同这个例子表明的一样,throw 不一定需要出现在 catch 的静态域中。


本文翻译自《Programming Ruby》,主要目的是自己学习使用,文中翻译不到位之处烦请指正,如需转载请注明出处

本章原文为 Exceptions, Catch, and
Throw

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值