# **第 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)。在這類編程語言中,大部分程序代碼都被花費在錯誤處理上,因此往往會使程序變得繁雜。

**圖 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` 捕捉。

**圖 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 標準庫中的異常類的繼承關系。

**圖 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 參考集
- 后記
- 謝辭