2020年4月13日

rust09 Error Handling

rust 有很多特性可處理錯誤狀況。通常 rust 希望你在編譯前,就找出很多錯誤。

rust 將錯誤分成兩類:recoverable and unrecoverable errors。recoverable error 通常是向 user 報告錯誤並重試,例如找不到檔案。unrecoverable error 就是 bug,例如存取超過 array 長度的 index。

大部分程式語言不區分這兩種錯誤,以 exception 方式處理。但 Rust 沒有 exception。對於 recoverable error 提供 Result<T, E> 及遇到 unrecoverable error 的 panic!,會停止程式執行。

以下會先介紹 panic!,然後說明如何回傳 Result<T, E>

Unrecoverable Errors with panic!

有時程式出問題,沒辦法處理,rust 可使用 panic! macro 列印錯誤訊息,unwind 並清理 stack,然後 quit。通常是發生在檢測到有 bug,但 programmer 不知道怎麼處理。

unwinding the Stack or Aborting in Response to a Panic

出現 panic 時,程式會開始 unwinding,rust 會開始回朔 stack 並清理每個函數中的資料,但這個動作會耗費很多時間。另一種處理方式是直接 abort,不清理資料直接 quit,由 OS 清理程式使用的 memory。如果希望讓 binary 很小,可在 Cargo.toml 的 [profile] 加上 panic='abort'

[profile.release]
panic = 'abort'

以簡單程式測試 panic!

fn main() {
    panic!("crash and burn");
}

執行結果

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.89s
     Running `target/debug/guessing_game`
thread 'main' panicked at 'crash and burn', src/main.rs:2:4
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

panic 出現的位置是 src/main.rs:2:4 ,也就是 src/main.rs 文件的第二行第四個字元。

panic! 可能是由 library或其他macro 呼叫的,可使用 panic! 的 backtrace 找到出問題的地方。

使用 panic! 的 backtrace

這是存取超過 vector 長度的 panic!

fn main() {
    let v = vec![1, 2, 3];

    v[99];
}

執行結果:發生 buffer overread,導致安全漏洞

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 1.04s
     Running `target/debug/guessing_game`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', /rustc/6c2484dc3c532c052f159264e970278d8b77cdc9/src/libcore/slice/mod.rs:2539:10
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

可設定 RUST_BACKTRACE=1 環境變數,列印 backtrace

$ RUST_BACKTRACE=1 cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', /rustc/6c2484dc3c532c052f159264e970278d8b77cdc9/src/libcore/slice/mod.rs:2539:10
stack backtrace:
   0: std::sys::unix::backtrace::tracing::imp::unwind_backtrace
             at src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:39
   1: std::sys_common::backtrace::_print
             at src/libstd/sys_common/backtrace.rs:70
   2: std::panicking::default_hook::{{closure}}
             at src/libstd/sys_common/backtrace.rs:58
             at src/libstd/panicking.rs:200
   3: std::panicking::default_hook
             at src/libstd/panicking.rs:215
   4: <std::panicking::begin_panic::PanicPayload<A> as core::panic::BoxMeUp>::get
             at src/libstd/panicking.rs:478
   5: std::panicking::continue_panic_fmt
             at src/libstd/panicking.rs:385
   6: std::panicking::try::do_call
             at src/libstd/panicking.rs:312
   7: <T as core::any::Any>::type_id
             at src/libcore/panicking.rs:85
   8: <T as core::any::Any>::type_id
             at src/libcore/panicking.rs:61
   9: <usize as core::slice::SliceIndex<[T]>>::index
             at /rustc/6c2484dc3c532c052f159264e970278d8b77cdc9/src/libcore/slice/mod.rs:2539
  10: core::slice::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/6c2484dc3c532c052f159264e970278d8b77cdc9/src/libcore/slice/mod.rs:2396
  11: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
             at /rustc/6c2484dc3c532c052f159264e970278d8b77cdc9/src/liballoc/vec.rs:1677
  12: panic::main
             at src/main.rs:4
  13: std::rt::lang_start::{{closure}}
             at /rustc/6c2484dc3c532c052f159264e970278d8b77cdc9/src/libstd/rt.rs:64
  14: std::panicking::try::do_call
             at src/libstd/rt.rs:49
             at src/libstd/panicking.rs:297
  15: panic_unwind::dwarf::eh::read_encoded_pointer
             at src/libpanic_unwind/lib.rs:87
  16: <std::panicking::begin_panic::PanicPayload<A> as core::panic::BoxMeUp>::get
             at src/libstd/panicking.rs:276
             at src/libstd/panic.rs:388
             at src/libstd/rt.rs:48
  17: std::rt::lang_start
             at /rustc/6c2484dc3c532c052f159264e970278d8b77cdc9/src/libstd/rt.rs:64
  18: panic::main

這些資訊只在 debug 版本會產生,也就是不要使用 --release 參數。

在第 12 行有指出發生問題的地方

  12: panic::main
             at src/main.rs:4

Recoverable Errors with Result

大部分的錯誤沒有嚴重到要停止程式。Result enum 有兩個成員: OkErr

enum Result<T, E> {
    Ok(T),
    Err(E),
}

T, E 是泛型類別參數。

文件 得知 File::open 會回傳 Result,直接編譯程式,也可以知道類別不匹配的錯誤。

如果刻意把 f 類別改成 u32,let f: u32 = File::open("hello.txt");可以由 compiler 得知錯誤的問題點,該 method 會回傳 std::result::Result<std::fs::File, std::io::Error>

error[E0308]: mismatched types
 --> src/main.rs:4:17
  |
4 |     let f:u32 = File::open("hello.txt");
  |                 ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum `std::result::Result`
  |
  = note: expected type `u32`
             found type `std::result::Result<std::fs::File, std::io::Error>`

error: aborting due to previous error
use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("There was a problem opening the file: {:?}", error)
        },
    };
}

執行時會得到自訂的 panic! 錯誤訊息

     Running `target/debug/panic`
thread 'main' panicked at 'There was a problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:9:13

也可以匹配 error 的類別,判斷不同類型的 error

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
            },
            other_error => panic!("There was a problem opening the file: {:?}", other_error),
        },
    };
}

這裡用 error.kind() 區分了 ErrorKind::NotFound 及其他錯誤

Shortcuts for Panic on Error: unwrap and expect

match 語法有點冗長。Result<T, E> 定義了其他 method 來輔助處理錯誤。

unwrap 會回傳 Ok 裡面的值,如果 ResultErrunwrap 會呼叫 panic!

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}

執行結果

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/libcore/result.rs:997:5
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

expec 可自訂 panic!的訊息內容

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

執行結果

thread 'main' panicked at 'Failed to open hello.txt: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/libcore/result.rs:997:5

Propagating Errors 傳遞錯誤

除了在函數中處理錯誤外,也可以選擇讓呼叫 method 的程式決定如何處理錯誤,這稱為 propagating error。

以下是從檔案中讀取 username 的函數,如果檔案不存在或無法讀取,會回傳錯誤。用 match 將錯誤回傳給呼叫者。

use std::io;
use std::io::Read;
use std::fs::File;

// 回傳 Result<String, io::Error>,也就是 Result<T, E>
fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    // 用 match 處理 File::open 的回傳值
    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    // 產生新的 String
    let mut s = String::new();

    // read_to_string 也會回傳 Result
    // 因為這是函數內最後一個 expression,不需要寫 return
    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

Shortcut for Propagating Errors: the ? Operator

跟上面的例子功能一樣,但使用了 ?

Result 值之後的 ? 被定義為與上一個例子中定義的處理 Result 值的 match 表達式有著完全相同的工作方式,如果是 Err 會直接回傳給呼叫者。

? 的錯誤值是傳給 from 函數,定義於 From trait,可將錯誤轉型。from 可將錯誤轉換為 io:Error

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

也可以這樣更縮減語法

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
}

rust 有提供另一個簡便的 method,將檔案讀入 String。

use std::io;
use std::fs;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}

? 只能用在回傳 Result 的函數

因為 ? 只能用在回傳 Result 的函數中,因此如果用在 main,將會發生編譯錯誤

use std::fs::File;

fn main() {
    let f = File::open("hello.txt")?;
}

編譯錯誤

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`)
 --> src/main.rs:4:13
  |
4 |     let f = File::open("hello.txt")?;
  |             ^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
  |
  = help: the trait `std::ops::Try` is not implemented for `()`
  = note: required by `std::ops::Try::from_error`

error: aborting due to previous error

要改用 match 語法,或是修改 main 函數,讓他回傳 Result<T, E>

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let f = File::open("hello.txt")?;

    Ok(())
}

Box<dyn Error> 稱為 trait object -> chap 17

什麼時候要使用 panic!

如果程式發生 panic,就表示無法恢復。比較好的方式,是使用 Result,讓呼叫者自己決定要怎麼處理。

有些狀況比較適合用 panic!,但不常見。

examples, prototype code, tests

如果是範例,使用 Result 會讓例子不明確。

在決定如何處理錯誤之前,unwrapexpect 在 prototype 中很適合。

如果 method 在測試中失敗了,就明確讓該測試失敗。因為 panic! 就代表測試失敗,因此要確切地使用 unwrapexpect

有明確的商業邏輯時

當有明確的邏輯,確定 Result 一定是 Ok 時,可使用 unwrap,因 compiler 無法知道這種邏輯。但實際上,還是有可能會發生呼叫失敗的狀況。

use std::net::IpAddr;

let home: IpAddr = "127.0.0.1".parse().unwrap();

因為 "127.0.0.1" 確實是有效的 IP Address,就直接使用 unwrap

Guidelines for Error Handling

在可能導致有害的狀況下,要使用 panic!,bad state 就是某些假設條件、保證、合約或不變性被破壞時,例如 invalid values, 自相矛盾的值或傳遞了不存在的值。還有

  • bad state 不是某些預期爾會發生的狀況
  • 後面的程式以這種有害狀況為條件,繼續執行
  • 沒有好的方法,可將這種資訊編碼,成為可使用的資料

如果別人呼叫你的程式,並傳遞了無效的值,最好的方式就是 panic!,警告使用這個 library 的人,有 bug。同理,panic! 非常適合呼叫不能控制的外部程式碼時,因無法修復其回傳的無效狀態。

如果預期錯誤會發生,就要用 Result。例如解析器收到錯誤資料,或 http request 觸發了 rate limit。這時就將錯誤進行 propagation,讓呼叫者決定要怎麼處理。

當程式碼使用某些 values 前,要先檢查是不是 valid values,如果不是,就要 panic!。這是基於安全性的理由:嘗試使用無效資料,會造成安全漏洞。ex: out-of-bounds memory access。

函數通常會遵循 contracts,該行為只會在輸入資料滿足某些條件,才能正常運作。違反契約,就發生 panic 是合理的,這代表呼叫方有 bug。也沒有合理的方法可以恢復呼叫方的程式碼。

雖然函數中有很多次錯誤檢查很煩人。可用 rust 類別系統及 compiler 類別檢查協助。如果函數已經有特定類別的參數,compiler 會檢查一定有有效的 value。例如使用不同於 Option 的類別,程式期望有值而不是 None,程式碼不需要處理 Some, None 兩種狀況,compiler 可確保一定有值,因為無法向函數傳遞空值。另外像 u32 也可以確保永遠不會是負數。

先前用 if 判斷數字是否超過有效範圍

loop {
    // --snip--

    let guess: i32 = match guess.trim().parse() {
        Ok(num) => num,
        Err(_) => continue,
    };

    if guess < 1 || guess > 100 {
        println!("The secret number will be between 1 and 100.");
        continue;
    }

    match guess.cmp(&secret_number) {
    // --snip--
        }
}

更有效的方法,是建立新的類別,並將資料檢查放到 constructor 中。以下只有 1 ~ 100 的數字,才能建立新的 Guess 類別

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess {
            value
        }
    }

    // getter 的功能,因為 value 是私有的
    pub fn value(&self) -> i32 {
        self.value
    }
}

References

The Rust Programming Language

中文版

中文版 2

沒有留言:

張貼留言