iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 18
0
Software Development

30 天把自己榨好榨滿的四週四語言大挑戰!系列 第 18

[Day 17] 發生問題趕快舉手!

今天的主題在 Hackerrank 的安排下是延續昨天的主題。昨天我們做的事情是當發現異常的時候,各種語言是用什麼樣的方式在解決。然而今天我們要來看看如何讓我們自己的程式能夠拋出異常來讓其他程式做後續處理囉!今天也會更深入地探討這四個語言在異常處理還有什麼有趣之處。那就讓我們開始吧!


Python 3

class Calculator(object):
    def power(self, n, p):
        if (n < 0) or (p < 0):
            raise Exception("n and p should be non-negative")
        else:
            return n ** p


myCalculator = Calculator()
time = int(input())
for i in range(time):
    n, p = map(int, input().split())
    try:
        ans = myCalculator.power(n, p)
        print(ans)
    except Exception as e:
        print(e)
  • 上述的程式碼是先宣告一個 class Calculator,裏頭有一個 Method power 是用來計算 np 次方的值是多少。然而我們並不希望 np 其中有人是負的,如果有人是負的,那麼我們就會試圖拋出一個 Exception,反之則返回正確的值。我們可以看到 raise Exception("n and p should be non-negative") 就是在拋出異常囉!這裡的關鍵字是 raise。這裡我們可以做出自己的 Exception,只要繼承自 BaseException 就可以了。像是常見的 KeyboardInterrupt 也是繼承自 BaseException,也就是當我們想要中斷程式有時候會按 ctrl-c ,系統就會拋出這個異常囉!然而如果我們今天只是用 try...except,而沒有特別指明要比對的 Exception 的類型,那麼在例如下面的程式,一個無窮迴圈內含一個可以捕捉全部 Exception 的 try...except 就無法透過 KeyboardInterrupt,通常是 ctrl-c,來中斷程式了。
while True:
    try:
        ...
    except:
        ...
  • 回到程式中,map(int, input().split()) 的意思是將第二個參數 input().split(),也就是 User 輸入的字串做分割後得到的 List,其中每一個元素都套用第一個參數,也就是 int 這個 Function,來得到一個新的 List,跟 Scala 的 map 是一樣的效果。最後看到 except Exception as e,這裡當 Exception 被捕獲的時候,將其賦予給 e,供 except 的程式區塊去使用囉!在這裡就是簡單地將 e 給印出來,也就是 n and p should be non-negative

Scala

  • 在 Scala 如果要拋出一個 Exception,用的關鍵字就是 throw,例如當程式啟動時,參數不符合預期,我們可以 throw IllegalArgumentExceptionIllegalArgumentException 繼承了 Exception 這個 class,所以如果我們要客製出一個自己的 Exception 就要使之繼承 Exception。例如下面,其中 e 是該 Exception 的文字解釋,可以在 Match exception 的時候將 e 印出。
class CustomException(s: String)  extends Exception(s)
  • 但是在 Scala,先前一直強調 Functional programming,如果在 Scala 我們選擇直接拋出異常,那麼這樣的做法就不是 Functional way 了。所以取而代之的是,我們應該返回一個代表錯誤的值,但又不是把錯誤編碼在本來返回的值之中 (like C 語言)。Scala 提供了一個特殊的類型來代表可能會有異常的計算,也就是 Try 這個類型。記得我們講過 Option 嗎?可能有值也可能沒值的時候就用 OptionTry 是一樣的道理:可能會有異常也可能成功地拿到結果。Try[A] (A 是 Type parameter,也就是泛型,之後會提到),表示結果的類型是 A,例如 Int,而 Try[A] 有兩個子類型 Success[A] 和封裝了 Exception 的 Failure[A]。所以假使我們知道函式可能會有異常的結果,我們就讓返回值是 Try 類型,這樣子呼叫者就知道必須要處理可能異常的狀況了!例如下面的 parseInt,我們用了 Tryapply Method (記得語法糖嗎),把可能會產生異常的計算當作參數,假如今天計算有了異常,這個 apply Method 就會捕獲異常,並返回 Failure[Int]。至於呼叫者拿到返回結果後,最常見的方式就是透過 getOrElse 這個 Try 的 Method 來得到成功的結果或是當失敗的時候拿到一個預設值,也就是下面 getOrElse 後面的 0。當然你也可以用 pattern matching 的方式,或把 Try 當成像是 Option 或其他 Collection 的類型去做後續處理,像是 mapflatMap 之類等等。可以參考這裡囉!
def parseInt(s: String): Try[Int] = Try(Integer.parseInt(s.trim))
val i = parseInt("xxx") getOrElse 0

Golang

  • 因為 Golang 沒有 Exception,所以今天讓我們來更深入看看 Golang 中跟 Error handling 相關的部分吧!上次提到,在 Golang 的世界,由於一個 Function 是可以回傳多個結果,所以要是 Function 可能會有異常,就會以其中一個回傳值來去告訴呼叫者是否有 Error 產生,並且由呼叫者先檢查回傳的 Error,有的話則處理,沒有才能去使用真正的回傳值。所以以下的程式在 Golang 是相當常見的。如果你要忽略 Error,也必須顯性地表明,以 Blank Identifier 來去接收 Error,也就是下面 val2, _ := myFunction(...)_,但不建議忽略。另外在 Golang 也可以使用 panic 來讓程式遇到無法恢復的錯誤時將程式中斷,一般不建議直接使用。
val1, err := myFunction(...)
if err != nil {
  // handle error
}

val2, _ := myFunction(...)
  • 至於 Error 在 Golang 是個 interface,之後我們會再細講 Golang 的 interface。我們可以先看到在 error 這個 interface 裡面有一個 Error() string 的函式簽名,只要一個 Struct 有實作 Error() string 我們就說這個 Struct 可以是個 error。常見的例如 Built-in 的 errors.New(<error message>) 就會返回 errorString Struct 的 Pointer,而這個 Struct 的 Pointer 也實作了 Error() string 這個函式。fmt.Errorf(<error message>) 也是。
type error interface {
    Error() string
}
  • 最後我們來提下 Type assertion 和 Type switch。上面提到只要實作了 interface error 的 Struct 就可以當作 error 來返回,但是如果我們想知道究竟這個 error 是哪個 Type 的 Struct 呢?就要用到 Type assertion,例如下面的程式,定義了兩個 Type NetworkErrorFileError,且都實作了 error interface。而執行 val, err := MyFunction()後得到的 err 可能是 nil, *NetworkError*FileError。透過 err, ok := err.(*NetworkError),假設 err*NetworkErrorok 的值就會是 true,就可以後續把 error 當作是 *NetworkError 來存取。如果我們想要根據不同的 Type 去做對應的事,就可以透過去 switch err.(type) 來進行,如同下面的例子囉! 更多關於 Golang 如何做 Error Handling 可以參考這裡
type NetworkError struct { n string }
type FileError struct { f string }

func (networkError *NetworkError) Error() string { ... }
func (fileError *FileError) Error() string { ... }

val, err := MyFunction() // may return *NetworkError or *FileError
err, ok := err.(*NetworkError) 
if ok {
    fmt.Println(err.n)
}

switch _, err := MyFunction(); err.(type) {
    case nil:
        ...
    case *NetworkError:
        ...
    case *FileError:
        ...
}

Rust

  • 在 Rust 如果我們遇到不可恢復的錯誤時,我們會用 panic!() 這個 Macro (panic!() 是 thread-based,所以在多 Thread 時,其中一個 Thread 被 Panic 時,其他還是可以繼續運行),使用上 panic!(<some message>);。還有一些 Macro 也可以有 panic!()的效果,但是有更具體的語意,像是 unimplemented!(),表示這一部分還沒有實作 (會 show not yet implemented);unreachable!() 可以表示某段 Code 不應該進入 (會 show internal error: entered unreachable code);還有像是 assert!() 等等。
  • 以上談到的都是不可恢復的錯誤才會用 Panic,如果是可恢復的就應該利用 Option (SomeNone) 或 Result (OkErr)。Option 是在當函式可以回傳空值時使用 (假使一個函式的參數是 Optional 也可以用)。Result 則是當函式可以回傳 Error 的情況下去使用。這兩個在接收後都可以利用 Pattern matching 的方式去做判斷,或是 Rust 有提供 is_some()is_none() (for Option) 和 is_ok()is_err() (for Result) 來讓你檢視。而 Result 也有 Method ok() 來將 Result 轉成是 Option (Ok => SomeErr => None)
  • 最後談到如果我們每次 Pattern matching 都只是要在 SomeOk 的時候取值,NoneErr 的時候引發 Panic,可以直接拿 OptionResult 的 Instance 去呼叫 unwrap()expect(<some message>) 來取代每次繁瑣的 Pattern matching (如果有另外的處理方式當然還是要寫),此外還有像是 unwrap_or() (回傳一個預設值而不是 Panic)。
  • 還有更多可以參考這裡囉!

上一篇
[Day 16] 知錯能改善莫大焉
下一篇
[Day 18] 疊起來還是排起來
系列文
30 天把自己榨好榨滿的四週四語言大挑戰!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言