![](/media/images/2023-08-29-image-11.jpg.webp)
Rust的例外處理-可復原類型的錯誤
Rust Recoverable Error Handling
跟Python不一樣的地方在於,Rust沒有try-catch
結構的例外處理機制,取而代之的是將錯誤分成兩種類型:
- 可復原的錯誤
- 不可復原的錯誤
然後針對這兩種錯誤進行不同的處理方式。
可復原的錯誤 (Recoverable)
沒有嚴重到要讓整個程序停止的錯誤。
例如:試圖開啟不存在的檔案路徑
Rust提供了Result
這個型別處理這種類型的錯誤:
enum Result<T, E> {
Ok(T),
Err(E),
}
- T表示成功的時候會回傳的型別
- E表示失敗時的回傳型別
透過這些泛型參數,就可以將Result
用在不同的場合來處理成功與失敗的結果後,回傳不相同的型別。
舉例來說:
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
}
- 假設
hello.txt
存在,greeting_file_result
就會是File Handle - 不存在的話,就會是包含該錯誤資訊的
Err
Instance
因此,我們需要在外面額外撰寫不同的處理邏輯,針對不同的結果採取不同的動作:
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("開啟檔案時發生問題:{:?}", error),
};
}
利用match
表達式,我們可以針對Ok
或是Err
變體做不同的處理。
配對不同的錯誤
File::open
在Err
變體的回傳型別為io::Error
,這是標準函式庫提供的結構體。這個結構有一個kind
方法,可以讓我們取得io::ErrorKind
的數值,裡面包含了各種io
過程可能發生的錯誤類型,例如ErrorKind::NotFound
。
我們可以用繼續用match
去針對不同的io類型錯誤做客製化的處理。
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("建立檔案時發生問題:{:?}", e),
},
other_error => {
panic!("開啟檔案時發生問題:{:?}", other_error);
}
},
};
}
把邏輯改為:
- 試圖打開hello.txt
- 要是打開失敗,而且錯誤是NotFound
就建立新的檔案
- 建立的過程也有可能失敗,所以再match一次錯誤
- 成功的話回傳file handle
- 要是繼續出錯,直接Panic
- 其他錯誤就直接panic!
Match以外的選擇,使用Closure配對Result<T, E>
match
表達式有可能隨著需求變多,導致程式碼囉唆起來。這個時候我們可以用標準函式庫提供的unwrap_or_else
搭配Closure
的功能,讓程式碼便的簡潔不少。
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("建立檔案時發生問題:{:?}", error);
})
} else {
panic!("開啟檔案時發生問題:{:?}", error);
}
});
}
Match以外的選擇,unwrap與expect
Result<T ,E>
型別本身就有非常多的輔助方法來執行不同的特定任務,例如unwrap()
。
Result.unwrap()
如果Result
的結果為Ok
,unwrap
就回傳Ok裡面的數值。如果是Err
,unwrap
就會呼叫panic
。
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
Result.expect()
expect
跟unwrap
類似,但是他的目的著重在panic!
時的處理。
使用expect
可以讓你在發生panic!
的時候,提供更完善的錯誤訊息,例如:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt 應該要存在此專案中");
}
官方文件特地表明了,通常在正式環境的CodeBase中,多數人會使用expect讓出錯時可以有更多資訊查看,而非使用unwrap
Propagating傳遞錯誤
有時候你可能不會想要直接在函數內處理錯誤,而是會想把錯誤往外拋出去給呼叫的程式碼處理,這樣的行為稱之為Propagating
。
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
}
- 跟上面的範例不同,我們把
panic
的部分都拿掉了 - 中間可以看到
username_file_result
的處理過程中如果碰到錯誤,會直間返回Err(e)
?運算子
因為中途使用reutrn Err(e)
的情境因為很常見,所以Rust提供了?
運算子來簡化了這段的流程:
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
}
?
運算符直接取代了原本的match
,Ok
,Err(e)
File::open
出問題的話,就會直接使用return關鍵字回傳Err
另外這段程式碼還可以更簡潔的串連再一起:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
?運算子的限制
只能用在函數回傳值相容於
?
使用的數值才行
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
- main函數的回傳值是
()
?
有可能會直接return Err
,也就是說回傳值的部分跟main會不匹配
錯誤訊息:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let greeting_file = File::open("hello.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` due to previous error
- 編譯器告訴我們「只能在回傳型別為
Result
或Option
或其他有實作FromResidual
的型別的函式才能使用?
運算子」