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 有兩個成員: Ok
及 Err
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
裡面的值,如果 Result
是 Err
,unwrap
會呼叫 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
會讓例子不明確。
在決定如何處理錯誤之前,unwrap
跟 expect
在 prototype 中很適合。
如果 method 在測試中失敗了,就明確讓該測試失敗。因為 panic!
就代表測試失敗,因此要確切地使用 unwrap
與 expect
有明確的商業邏輯時
當有明確的邏輯,確定 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
}
}
沒有留言:
張貼留言