美文网首页
Exceptions, Catch, and Throw

Exceptions, Catch, and Throw

作者: Cloneable | 来源:发表于2020-09-05 18:53 被阅读0次

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

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

    传统方式都是返回错误码。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

    相关文章

      网友评论

          本文标题:Exceptions, Catch, and Throw

          本文链接:https://www.haomeiwen.com/subject/iemfektx.html