# **第 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` 中调用时,会再次抛出最后一次发生的异常(`$!`)。
- 推荐序
- 译者序
- 前言
- 本书的读者对象
- 第 1 部分 Ruby 初体验
- 第 1 章 Ruby 初探
- 第 2 章 便利的对象
- 第 3 章 创建命令
- 第 2 部分 Ruby 的基础
- 第 4 章 对象、变量和常量
- 第 5 章 条件判断
- 第 6 章 循环
- 第 7 章 方法
- 第 8 章 类和模块
- 第 9 章 运算符
- 第 10 章 错误处理与异常
- 第 11 章 块
- 第 3 部分 Ruby 的类
- 第 12 章 数值类
- 第 13 章 数组类
- 第 14 章 字符串类
- 第 15 章 散列类
- 第 16 章 正则表达式类
- 第 17 章 IO 类
- 第 18 章 File 类与 Dir 类
- 第 19 章 Encoding 类
- 第 20 章 Time 类与 Date 类
- 第 21 章 Proc 类
- 第 4 部分 动手制作工具
- 第 22 章 文本处理
- 第 23 章 检索邮政编码
- 附录
- 附录 A Ruby 运行环境的构建
- 附录 B Ruby 参考集
- 后记
- 谢辞