iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 17
1

今天我們來看看 Error handling 的部分吧!也就是如何處理異常和錯誤。異常和錯誤又有點不一樣,錯誤往往是指非預期的情況,必須透過修改程式來解決,而異常比較是不規則的,是可以預期的,例如讀取檔案,本來就是有可能會因為沒有檔案而無法讀取。有一些語言是以拋出預先定義或是自定義的 Exception,將邏輯以及處理錯誤的程式分開,處理錯誤的程式捕獲 Exception 後進行處理,像是 Python 和 Scala。而另一種常見的則是返回錯誤,像是 Golang 以及 Rust,由程式的呼叫者自行了解到錯誤並進行處理。現在就讓我們來看看吧!


Python

try:
    print(int(input().strip()))
except ValueError:
    print("Bad String")
  • 如同我們前言所說的,Python 的異常狀況往往是以拋出 Exception 的方式來被處理。因為這並非程式的錯誤,只是異常狀況,所以我們必須要將 Exception 捕獲並處理,而不是終止程式。
  • 在以上的程式碼,我們原先希望 User 輸入的會是一個整數,然而當 User 輸入一個一般的字串時,我們也會遇期程式會拋出錯誤,也就是程式碼中的 ValueError。而在 Python 我們用的是 try...except,來去執行我們預期會拋出 Exception 的程式,並且在 except 區塊去比對 Exception 的型態。這裡因為我們已經知道會拋出的型態是 ValueError,所以可以這樣寫。如果沒有加上 ValueError 就表示只要有任何型態的 Exception 都會被捕獲,並且執行區塊內的程式碼。假使我們知道有多種 Exception 需要做不同的處理,就可以寫多個 except <Exception Type> 來去處理不同狀況,並且在最後有一個單獨的 except 來處理當沒有比對到任何已知的 Exception 型態時要做什麼事。假如沒有這麼做,最後沒有捕捉到的話,就會出現 Traceback 並且中斷程式囉!
  • 除了 try...except 還可以加上 finally,也就是當不管有沒有拋出異常,都一定會執行的部分,大多數情況會用來關閉資源。例如下面的程式不管怎樣都會在 finally 去做關閉。這裡順道提一下就是因為關閉資源已經變成很多資源開啟後的例行工作,所以 Python 提供了 with 這個語法讓我們不用再自己去寫像是 file.close() 之類的程式,可以參考這裡
file = open('test.py', 'r', encoding='UTF-8')
try:
    for line in file:
        print(line, end='')
except:
    print('Failed to read file')
finally:
    file.close()
  • 如果我們要自己在程式中拋出 Exception 的話,可以使用 raise <Exception Type> 來完成。

Scala

def parseInteger(s: String): Option[Int] = {
    try {
        Some(Integer.parseInt(s.trim))
    } catch {
        case e: Exception => None
    }
}
  • 假設我們有一個 Function 叫做 parseInteger 是幫我們把字串轉成整數,而回傳的 Type 是 Option[Int]。我們之前說過 Option 有兩個 Subclass 分別是 SomeNone,如果有值就是 Some(<value>)反之則是 None。而在這個 Function 裏頭是一個 try...catch 的語法,相當於 Python 的 try...except,要是 try 區塊的程式拋出了 Exception,則會執行catch的內容,而如同 Python,在 catch 裡頭也可以去比對是什麼樣的 Exception。這邊 case e: Exception => None 指的就是假設 Exception 是 Exception 就回傳 None。我們也可以寫成 case e: NumberFormatException => None 也就是把真正的 Exception 類型寫出來。(Exception 是所有 Exception 的父類別) 因此可以比對不同類型的 Exception 做相對應的處理。那麼為什麼我們要特地將 try...catch 包一層並回傳 Option 呢?因為這樣的寫法才符合 Functional way,也就是執行 Function 的人一定會拿到回傳值,而不是得到 null 或是一個 Exception。

Golang

i, err := strconv.Atoi(s)
   if err != nil {
      // Error handling
   }
  • Golang 不像是 Python 和 Scala 是拋出異常,而是當遇到異常的時候以返回值的一部份讓呼叫函式的人了解到並處理。我們可以看到 strconv 這個 Package 文件中的 Atoi 方法,是用來把字串轉成整數,而其回傳兩個值,第一個是轉換出來的整數,第二個則是一個 errorerror 是個 interface (之後會細講 interface),目前可以先當作一個類型。假使轉換整數成功,第二個參數就會回 nil,表示沒有 Error。所以如同上面程式碼,常見的方式會是判斷 err != nil 來做錯誤處理。
  • 假使函式的定義說會回傳 Error,那就一定要處理,不然繼續執行下去的話可能就會發生錯誤。比起拋出 Exception,雖然寫起來比較瑣碎,但是可讀性大增,不用去特別了解到函式是否會拋出 Exception ,只要從 Function 的 Signature 就能夠知道需不需要處理 Error。呼叫者本身也直接有責任對 Error 進行處理,不會像 Exception 有可能一路往上層拋,如果不幸沒有捕獲到那就會造成程式中斷。

Rust

fn toInt(number_str: &str) -> i32 {
    match number_str.parse::<i32>() {
        Ok(n) => n,
        Err(err) => 0,
    }
}
  • Rust 有分做可恢復以及不可恢復的錯誤,不可恢復用的是 panic! 這個 Macro,這個我們之後會再談。今天要談的是可恢復的,也就是可以經由得知錯誤後進行處理,而在 Rust 並沒有 Exception,而是回傳 Result 這個枚舉類型。沒錯!我們之前談過了 Result 分別有 OkErr 兩種 variant。如果要呼叫一個可能出錯的函式,那麼其就應該回傳 Result。這樣的好處是,因為回傳的是 Result,所以一定要同時處理不管是 Ok 或是 Err,也就是說錯誤處理成了邏輯的一部分。以上面的程式為例,我們會透過 match 來去比對是 Ok 還是 Err 則作出對應的處理,不過就如同我們先前提過的,因為這樣寫比較瑣碎,所以可以寫成 number_str.parse::<i32>().unwrap_or(0),就相當於上述 match 的區塊囉!假使今天 Err 所帶的值 err 會根據不同錯誤狀況而不同時,也是可以在增加比對的分支,來對不同的錯誤進行處理囉!

上一篇
[Day 15] 手牽手心連心!
下一篇
[Day 17] 發生問題趕快舉手!
系列文
30 天把自己榨好榨滿的四週四語言大挑戰!30

尚未有邦友留言

立即登入留言