企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# **第 10 章 错误处理与异常** 程序在运行时会伴随着各种各样的错误发生。如果我们在编写程序时不犯任何错误,并且所有处理都能正常执行的话,那么就不会产生程序错误,但实际上并不可能有这么完美的程序。在本章中,我们将围绕着程序错误及其应对方法,向大家介绍一下 Ruby 异常处理的相关内容。 ### **10.1 关于错误处理** 在介绍实际的程序例子前,我们先来了解一下程序错误相关的基础知识。在程序执行的过程中,通常会有以下错误发生: - **数据错误** 在计算家庭收支的时候,若在应该写金额的一栏上填上了商品名,那么就无法计算。此外,HTML 这种格式的数据的情况下,如果存在没有关闭标签等语法错误,也会导致无法处理数据。 - **系统错误** 硬盘故障等明显的故障,或者没把 CD 插入到驱动器等程序无法恢复的问题。 - **程序错误** 因调用了不存在的方法、弄错参数值或算法错误而导致错误结果等,像这样,程序本身的缺陷也可能会导致错误。 程序在运行时可能会遇到各种各样的错误。如果对这些错误放任不管,大部分程序都无法正常运行,因此我们需要对这些错误做相应的处理。 - **排除错误的原因** 在文件夹中创建文件时,如果文件夹不存在,则由程序本身创建文件夹。如果程序无法创建文件夹,则需要再考虑其他解决方法。 - **忽略错误** 程序有时候也会有一些无伤大雅的错误。例如,假设运行程序时需要读取某个配置文件,如果我们事前已经在程序中准备好了相应配置的默认值,那么即使无法读取该设定文件,程序也可以忽略这个错误。 - **恢复错误发生前的状态** 向用户提示程序发生错误,指导用户该如何进行下一步处理。 - **重试一次** 曾经执行失败的程序,过一段时间后再重新执行可能就会成功。 - **终止程序** 只是自己一个人用的小程序,也许本来就没必要做错误处理。 而至于实际应该采取何种处理,则要根据程序代码的规模、应用程序的性质来决定,不能一概而论。但是,对于可预期的错误,我们需要留意以下两点: - **是否破坏了输入的数据,特别是人工制作的数据。** - **是否可以对错误的内容及其原因做出相应的提示。** 覆盖了原有文件、删除了花费大量时间输入的数据等,像这样的重要数据的丢失、破坏可以说是灾难性的错误。另外,如果错误是由用户造成的,或者程序自身不能修复的话,给用户简明易懂的错误提示,会大大提升程序的用户体验。 Ruby 为我们提供了异常处理机制,可以使我们非常方便地应对各种错误。 ### **10.2 异常处理** 在程序执行的过程中,如果程序出现了错误就会发生异常。异常发生后,程序会暂时停止运行,并寻找是否有对应的异常处理程序。如果有则执行,如果没有,程序就会显示类似以下信息并终止运行。 > **执行示例** ~~~ > ruby test.rb test.rb:2:in `initialize': No such file or directory - /no/file(Errno::ENOENT) from test.rb:2:in `open' from test.rb:2:in `foo' from test.rb:2:in `bar' from test.rb:9:in `main' ~~~ 该信息的格式如下: **文件名: 行号`:in` 方法名: 错误信息(异常类名)     `from` 文件名: 行号:`in` 方法名     ┊** 以 `from` 开头的行表示发生错误的位置。 没有异常处理的编程语言的情况下,编程时就需要逐个确认每个处理是否已经处理完毕(图 10.1)。在这类编程语言中,大部分程序代码都被花费在错误处理上,因此往往会使程序变得繁杂。 ![{%}](https://box.kancloud.cn/2015-10-26_562e01e8f1ad6.png) **图 10.1 异常处理** 异常处理有以下优点: - **程序不需要逐个确认处理结果,也能自动检查出程序错误** - **会同时报告发生错误的位置,便于排查错误** - **正常处理与错误处理的程序可以分开书写,使程序便于阅读** ### **10.3 异常处理的写法** Ruby 中使用 `begin ~ rescue ~ end` 语句描述异常处理。 **`begin`  可能会发生异常的处理 `rescue`  发生异常时的处理 `end`** 在 Ruby 中,异常及其相关信息都是被作为对象来处理的。在 `rescue` 后指定变量名,可以获得异常对象。 **`begin`  可能会发生异常的处理 `rescue =>` 引用异常对象的变量  发生异常时的处理 `end`** 即使不指定变量名,Ruby 也会像表 10.1 那样把异常对象赋值给变量 `$!`。不过,把变量名明确地写出来会使程序更加易懂。 **表 10.1 异常发生时被自动赋值的变量** <table border="1" data-line-num="92 93 94 95 96" width="90%"><thead><tr><th> <p class="表头单元格">变量</p> </th> <th> <p class="表头单元格">意义</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格"><code>$!</code></p> </td> <td> <p class="表格单元格">最后发生的异常(异常对象)</p> </td> </tr><tr><td> <p class="表格单元格"><code>$@</code></p> </td> <td> <p class="表格单元格">最后发生的异常的位置信息</p> </td> </tr></tbody></table> 此外,通过调用表 10.2 中的异常对象的方法,就可以得到相关的异常信息。 **表 10.2 异常对象的方法** <table border="1" data-line-num="101 102 103 104 105 106" width="90%"><thead><tr><th> <p class="表头单元格">方法名</p> </th> <th> <p class="表头单元格">意义</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格"><code>class</code></p> </td> <td> <p class="表格单元格">异常的种类</p> </td> </tr><tr><td> <p class="表格单元格"><code>message</code></p> </td> <td> <p class="表格单元格">异常信息</p> </td> </tr><tr><td> <p class="表格单元格"><code>backtrace</code></p> </td> <td> <p class="表格单元格">异常发生的位置信息(<code>$@</code> 与 <code>$!.backtrace</code> 是等价的)</p> </td> </tr></tbody></table> 代码清单 10.1 是 Unix 的 wc 命令的简易版。结果会输出参数中指定的各文件的行数、单词数、字数(字节数),最后输出全部文件的统计结果。 > **执行示例** ~~~ > ruby wc.rb intro.rb sec01.rb sec02.rb 50 67 1655 intro.rb 81 92 3455 sec01.rb 123 162 3420 sec02.rb 254 321 8520 total ~~~ **代码清单 10.1 wc.rb** ~~~ ltotal=0 # 行数合计 wtotal=0 # 单词数合计 ctotal=0 # 字数合计 ARGV.each do |file| begin input = File.open(file) # 打开文件(A) l=0 # file 内的行数 w=0 # file 内的单词数 c=0 # file 内的字数 input.each_line do |line| l += 1 c += line.size line.sub!(/^\s+/, "") # 删除行首的空白符 ary = line.split(/\s+/) # 用空白符分解 w += ary.size end input.close # 关闭文件 printf("%8d %8d %8d %s\n", l, w, c, file) # 整理输出格式 ltotal += l wtotal += w ctotal += c rescue => ex print ex.message, "\n" # 输出异常信息(B) end end printf("%8d %8d %8d %s\n", ltotal, wtotal, ctotal, "total") ~~~ 在(A)处无法打开文件时,程序会跳到 `rescue` 部分。这时,异常对象被赋值给变量 `ex`,(B)部分的处理被执行。 如果程序中指定了不存在的文件,则会提示发生错误,如下所示。提示发生错误后,并不会马上终止程序,而是继续处理下一个文件。 > **执行示例** ~~~ > ruby wc.rb intro.rb sec01.rb sec02.rb sec03.rb 50 67 1655 intro.rb 81 92 3455 sec01.rb 123 188 3729 sec02.rb No such file or directory - sec03.rb 254 321 8520 total ~~~ 如果发生异常的方法中没有 `rescue` 处理,程序就会逆向查找调用者中是否定义了异常处理。下面来看看图 10.2 这个例子。调用 `foo` 方法,尝试打开一个不存在的文件。若 `File.open` 方法发生异常,那么该异常就会跳过 `foo` 方法以及 `bar` 方法,被更上一层的 `rescue` 捕捉。 ![{%}](https://box.kancloud.cn/2015-10-26_562e01e911202.png) **图 10.2 异常处理的流程** 然而,并不是说每个方法都需要做异常处理,只需根据实际情况在需要留意的地方做就可以了。在并不特别需要解决错误的情况下,也可以不捕捉异常。当然,不捕捉异常就意味着如果有问题发生程序就会马上终止。 ### **10.4 后处理** 不管是否发生异常都希望执行的处理,在 Ruby 中可以用 `ensure` 关键字来定义。 **`begin`  有可能发生异常的处理 `rescue` => 变量  发生异常后的处理 `ensure`  不管是否发生异常都希望执行的处理 `end`** 现在,假设我们要实现一个拷贝文件的方法,如下所示。下面的 `copy` 方法是把文件从 `from` 拷贝到 `to`。 ~~~ def copy(from, to) src = File.open(from) # 打开原文件from(A) begin dst = File.open(to, "w") # 打开目标文件to(B) data = src.read dst.write(data) dst.close ensure src.close # (C) end end ~~~ 在(A)部分,如果程序不能打开原文件,那么就会发生异常并把异常返回给调用者。这时,不管接下来的处理是否能正常执行,`src` 都必须得关闭。关闭 `src` 的处理在(C)部分执行。`ensure` 中的处理,在程序跳出 `begin ~ end` 部分时一定会被执行。即使(B)中的目标文件无法打开,(C)部分的处理也同样会被执行。 ### **10.5 重试** 在 `rescue` 中使用 `retry` 后,`begin` 以下的处理会再重做一遍。 在下面的例子中,程序每隔 10 秒执行一次 `File.open`,直到能成功打开文件为止,打开文件后再读取其内容。 ~~~ file = ARGV[0] begin io = File.open(file) rescue sleep 10 retry end data = io.read io.close ~~~ 不过需要注意的是,如果指定了无论如何都不能打开的文件,程序就会陷入死循环中。 ### **10.6 rescue 修饰符** 与 `if` 修饰符、`unless` 修饰符一样,`rescue` 也有对应的修饰符。 **表达式 `1 rescue` 表达式 `2`** 如果表达式 1 中发生异常,表达式 2 的值就会成为整体表达式的值。也就是说,上面的式子与下面的写法是等价的: **`begin`  表达式 `1` `rescue`  表达式 `2` `end`** 我们再来看看下面的例子: ~~~ n = Integer(val) rescue 0 ~~~ `Integer` 方法当接收到 `"123"` 这种数值形式的字符串参数时,会返回该字符串表示的整数值,而当接收到 `"abc"` 这种非数值形式的字符串参数时,则会抛出异常(在判断字符串是否为数值形式时经常用到此方法)。在本例中,如果 `val` 是不正确的数值格式,就会抛出异常,而 0 则作为 = 右侧整体表达式的返回值。像这样,这个小技巧经常被用在不需要过于复杂的处理,只是希望简单地对变量赋予默认值的时候。 ### **10.7 异常处理语法的补充** 如果异常处理的范围是整个方法体,也就是说整个方法内的程序都用 `begin ~ end` 包含的话,我们就可以省略 `begin` 以及 `end`,直接书写 `rescue` 与 `ensure` 部分的程序。 **`def foo`  方法体 `rescue` => `ex`  异常处理 `ensure`  后处理 `end`** 同样,我们在类定义中也可以使用 `rescue` 以及 `ensure`。但是,如果类定义途中发生异常,那么异常发生部分后的方法定义就不会再执行了,因此一般我们不会在类定义中使用它们。 **`class Foo`  类定义 `rescue` => `ex`  异常处理 `ensure`  后处理 `end`** ### **10.8 指定需要捕捉的异常** 当存在多个种类的异常,且需要按异常的种类分别进行处理时,我们可以用多个 `rescue` 来分开处理。 **`begin`  可能发生异常的处理 `rescue Exception1, Exception2` => 变量  对`Exception1` 或者`Exception2` 的处理 `rescue Exception3` => 变量  对`Exception3` 的处理 `rescue`  对上述异常以外的异常的处理 `end`** 通过直接指定异常类,可以只捕捉我们希望处理的异常。 ~~~ file1 = ARGV[0] file2 = ARGV[1] begin io = File.open(file1) rescue Errno::ENOENT, Errno::EACCES io = File.open(file2) end ~~~ 在本例中,程序如果无法打开 `file1` 就会打开 `file2`。程序中捕捉的 `Errno::ENOENT` 以及 `Errno::EACCES`,分别是文件不存在以及没权限打开文件时发生的异常。 ### **10.9 异常类** 之前我们提到过异常也是对象。Ruby 中所有的异常都是 `Exception` 类的子类,并根据程序错误的种类来定义相应的异常。图 10.3 为 Ruby 标准库中的异常类的继承关系。 ![{%}](https://box.kancloud.cn/2015-10-26_562e01e92ba32.png) **图 10.3 异常类的继承关系** 在 `rescue` 中指定的异常的种类实际上就是异常类的类名。`rescue` 中不指定异常类时,程序会默认捕捉 `StandardError` 类及其子类的异常。 `rescue` 不只会捕捉指定的异常类,同时还会捕捉其子类。因此,我们在自己定义异常时,一般会先定义继承 `StandardError` 类的新类,然后再继承这个新类。 ~~~ MyError = Class.new(StandardError) # 新的异常类 MyError1 = Class.new(MyError) MyError2 = Class.new(MyError) MyError3 = Class.new(MyError) ~~~ 这样定义后,通过以下方式捕捉异常的话,同时就会捕捉 `MyError` 类的子类 `MyError1`、`MyError2`、`MyError3` 等。 ~~~ begin ┊ rescue MyError ┊ end ~~~ 在本例中, ~~~ MyError = Class.new(StandardError) ~~~ 上述写法的作用是定义一个继承 `StandardError` 类的新类,并将其赋值给 `MyError` 常量。这与使用在第 8 章中介绍过的 `class` 语句定义类的效果是一样的。 ~~~ class MyError < StandardError end ~~~ 使用 `class` 语句,我们可以进行定义方法等操作,但在本例中,由于我们只需要生成继承 `StandardError` 类的新类就可以了,所以就向大家介绍了这个只需 1 行代码就能实现类的定义的简洁写法。 ### **10.10 主动抛出异常** 使用 `raise` 方法,可以使程序主动抛出异常。在基于自己判定的条件抛出异常,或者把刚捕捉到的异常再次抛出并通知异常的调用者等情况下,我们会使用 `raise` 方法。 `raise` 方法有以下 4 种调用方式: - **raise message** 抛出 `RuntimeError` 异常,并把字符串作为 message 设置给新生成的异常对象。 - **raise 异常类** 抛出指定的异常。 - **raise 异常类,message** 抛出指定的异常,并把字符串作为 message 设置给新生成的异常对象。 - **raise** 在 `rescue` 外抛出 `RuntimeError`。在 `rescue` 中调用时,会再次抛出最后一次发生的异常(`$!`)。