[Rust 筆記] Rust 中的錯誤處理
Result 枚舉
Rust 中沒有提供類似於 Java、C++ 中的 Exception 機制,而是使用 Result
枚舉的方式來實現:
pub enum Result<T, E> {
/// Contains the success value
Ok(T),
/// Contains the error value
Err(E),
}
在使用時:
-
如果無錯誤則使用
Ok(T)
返回; -
如果存在錯誤,則使用
Err(E)
包裝錯誤類型返回;
例如:
examples/0_result.rs
#[derive(Debug)]
pub enum MyError {
Internal(String),
InvalidId(String),
}
fn add(num: i64) -> Result<i64, MyError> {
if num < 0 {
Err(MyError::InvalidId(String::from("Invalid num!")))
} else {
Ok(num + 100000)
}
}
fn main() -> Result<(), MyError> {
// fetch_id(-1)?;
let res = add(1)?;
println!("{}", res);
Ok(())
}
上面的代碼首先通過 MyError 枚舉定義了多個可能會出現的錯誤;
隨後,在 add
函數中:
-
當 num 小於 0 時返回錯誤;
-
否則給 num 增加 100000 並返回;
在上面的 let res = add(1)?;
中使用了 ?
操作符,他相當於是一個語法糖:
-
如果被調函數正常返回則調用
unwrap
獲取其值; -
反之,則將被調函數的錯誤直接向上返回(相當於直接 return Err);
即上面的語法糖相當於:
let res = match add() {
Ok(id) => id,
Err(err) => {
return Err(err);
}
};
錯誤類型轉換
上面簡單展示了 Rust 中錯誤的使用;
由於 Rust 是強類型的語言,因此如果在一個函數中使用 ?
返回了多個錯誤,並且他們的類型是不同的,還需要對返回的錯誤類型進行轉換,轉爲相同的類型!
例如下面的例子:
#[derive(Debug)]
pub enum MyError {
ReadError(String),
ParseError(String),
}
fn read_file() -> Result<i64, MyError> {
// Error: Could not get compiled!
let content = fs::read_to_string("/tmp/id")?;
let id = content.parse::<i64>()?;
}
fn main() -> Result<(), MyError> {
let id = read_file()?;
println!("id: {}", id);
Ok(())
}
上面的例子無法編譯通過,原因在於: read_to_string
和 parse
返回的是不同類型的錯誤!
因此,如果要能返回,我們需要對每一個錯誤進行轉換,轉爲我們所定義的 Error 類型;
例如:
examples/1_error_convert.rs
fn read_file() -> Result<i64, MyError> {
// Error: Could not get compiled!
// let content = fs::read_to_string("/tmp/id")?;
// let id = content.parse::<i64>()?;
// Method 1: Handling error explicitly!
let content = match std::fs::read_to_string("/tmp/id") {
Ok(content) => content,
Err(err) => {
return Err(MyError::ReadError(format!("read /tmp/id failed: {}", err)));
}
};
let content = content.trim();
println!("read content: {}", content);
// Method 2: Use map_err to transform error type
let id = content
.parse::<i64>()
.map_err(|err| MyError::ParseError(format!("parse error: {}", err)))?;
Ok(id)
}
上面展示了兩種不同的轉換 Error 的方法:
方法一通過 match 匹配手動的對 read_to_string
函數的返回值進行處理,如果發生了 Error,則將錯誤轉爲我們指定類型的錯誤;
方法二通過 map_err
的方式,如果返回的是錯誤,則將其轉爲我們指定的類型,這時就可以使用 ?
返回了;
相比之下,使用 map_err 的方式,代碼會清爽很多!
From Trait
上面處理錯誤的方法,每次都要對錯誤的類型進行轉換,比較麻煩;
Rust 中提供了 From Trait,在進行類型匹配時,如果提供了從一個類型轉換爲另一個類型的方法(實現了某個類型的 From Trait),則在編譯階段,編譯器會調用響應的函數,直接將其轉爲相應的類型!
例如:
examples/2_from_trait.rs
#[derive(Debug)]
pub enum MyError {
ReadError(String),
ParseError(String),
}
impl From<std::io::Error> for MyError {
fn from(source: std::io::Error) -> Self {
MyError::ReadError(source.to_string())
}
}
impl From<std::num::ParseIntError> for MyError {
fn from(source: std::num::ParseIntError) -> Self {
MyError::ParseError(source.to_string())
}
}
fn read_file() -> Result<i64, MyError> {
let _content = fs::read_to_string("/tmp/id")?;
let content = _content.trim();
let id = content.parse::<i64>()?;
Ok(id)
}
fn main() -> Result<(), MyError> {
let id = read_file()?;
println!("id: {}", id);
Ok(())
}
在上面的代碼中,我們爲 MyError 類型的錯誤分別實現了轉換爲 std::io::Error
和 std::num::ParseIntError
類型的 From Trait;
因此,在 read_file 函數中就可以直接使用 ?
向上返回錯誤了!
但是上面的方法需要爲每個錯誤實現 From Trait 還是有些麻煩,因此出現了 thiserror 以及 anyhow 庫來解決這些問題;
其他第三方庫
thiserror
上面提到了我們可以爲每個錯誤實現 From Trait 來直接轉換錯誤類型,thiserror
庫就是使用這個邏輯;
我們可以使用 thiserror 庫提供的宏來幫助我們生成到對應類型的 Trait;
例如:
examples/3_thiserror.rs
#[derive(thiserror::Error, Debug)]
pub enum MyError {
#[error("io error.")]
IoError(#[from] std::io::Error),
#[error("parse error.")]
ParseError(#[from] std::num::ParseIntError),
}
fn read_file() -> Result<i64, MyError> {
// Could get compiled!
let content = fs::read_to_string("/tmp/id")?;
let id = content.parse::<i64>()?;
Ok(id)
}
fn main() -> Result<(), MyError> {
let id = read_file()?;
println!("id: {}", id);
Ok(())
}
我們只需要對我們定義的類型進行宏標註,在編譯時這些宏會自動展開並實現對應的 Trait;
展開後的代碼如下:
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2018::*;
#[macro_use]
extern crate std;
use std::fs;
pub enum MyError {
#[error("io error.")]
IoError(#[from] std::io::Error),
#[error("parse error.")]
ParseError(#[from] std::num::ParseIntError),
}
#[allow(unused_qualifications)]
impl std::error::Error for MyError {
fn source(&self) -> std::option::Option<&(dyn std::error::Error + 'static)> {
use thiserror::__private::AsDynError;
#[allow(deprecated)]
match self {
MyError::IoError { 0: source, .. } => std::option::Option::Some(source.as_dyn_error()),
MyError::ParseError { 0: source, .. } => {
std::option::Option::Some(source.as_dyn_error())
}
}
}
}
#[allow(unused_qualifications)]
impl std::fmt::Display for MyError {
fn fmt(&self, __formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
#[allow(unused_variables, deprecated, clippy::used_underscore_binding)]
match self {
MyError::IoError(_0) => {
let result =
__formatter.write_fmt(::core::fmt::Arguments::new_v1(&["io error."], &[]));
result
}
MyError::ParseError(_0) => {
let result =
__formatter.write_fmt(::core::fmt::Arguments::new_v1(&["parse error."], &[]));
result
}
}
}
}
#[allow(unused_qualifications)]
impl std::convert::From<std::io::Error> for MyError {
#[allow(deprecated)]
fn from(source: std::io::Error) -> Self {
MyError::IoError { 0: source }
}
}
#[allow(unused_qualifications)]
impl std::convert::From<std::num::ParseIntError> for MyError {
#[allow(deprecated)]
fn from(source: std::num::ParseIntError) -> Self {
MyError::ParseError { 0: source }
}
}
#[automatically_derived]
#[allow(unused_qualifications)]
impl ::core::fmt::Debug for MyError {
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match (&*self,) {
(&MyError::IoError(ref __self_0),) => {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "IoError", &&*__self_0)
}
(&MyError::ParseError(ref __self_0),) => {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "ParseError", &&*__self_0)
}
}
}
}
fn read_file() -> Result<i64, MyError> {
let content = fs::read_to_string("/tmp/id")?;
let id = content.parse::<i64>()?;
Ok(id)
}
#[allow(dead_code)]
fn main() -> Result<(), MyError> {
let id = read_file()?;
{
::std::io::_print(::core::fmt::Arguments::new_v1(
&["id: ", "\n"],
&[::core::fmt::ArgumentV1::new_display(&id)],
));
};
Ok(())
}
#[rustc_main]
pub fn main() -> () {
extern crate test;
test::test_main_static(&[])
}
可以看到實際上就是爲 MyError 實現了對應錯誤類型的 From Trait;
thiserror 庫的這種實現方式,還需要爲類型指定要轉換的錯誤類型;
而下面看到的 anyhow 庫,可以將錯誤類型統一爲同一種形式;
anyhow
如果你對 Go 中的錯誤類型不陌生,那麼你就可以直接上手 anyhow 了!
來看下面的例子:
examples/4_anyhow.rs
use anyhow::Result;
use std::fs;
fn read_file() -> Result<i64> {
// Could get compiled!
let content = fs::read_to_string("/tmp/id")?;
let id = content.parse::<i64>()?;
Ok(id)
}
fn main() -> Result<()> {
let id = read_file()?;
println!("id: {}", id);
Ok(())
}
注意到,上面的 Result 類型爲 anyhow::Result
,而非標準庫中的 Result 類型!
anyhow
爲 Result<T, E>
實現了 Context
Trait:
impl<T, E> Context<T, E> for Result<T, E> where
E: ext::StdError + Send + Sync + 'static,
{
fn context<C>(self, context: C) -> Result<T, Error>
where
C: Display + Send + Sync + 'static,
{
// Not using map_err to save 2 useless frames off the captured backtrace
// in ext_context.
match self {
Ok(ok) => Ok(ok),
Err(error) => Err(error.ext_context(context)),
}
}
fn with_context<C, F>(self, context: F) -> Result<T, Error>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C,
{
match self {
Ok(ok) => Ok(ok),
Err(error) => Err(error.ext_context(context())),
}
}
}
在 Context
中提供了 context
函數,並且將原來的 Result<T, E>
轉成了 Result<T, anyhow::Error>
;
因此,最終將錯誤類型統一爲了 anyhow::Error
類型;
附錄
源代碼:
- https://github.com/JasonkayZK/rust-learn/tree/error
文檔:
-
https://rustwiki.org/zh-CN/rust-by-example/error/multiple_error_types.html
-
https://doc.rust-lang.org/book/ch09-00-error-handling.html
-
https://nick.groenen.me/posts/rust-error-handling/
-
https://docs.rs/thiserror/latest/thiserror/
-
https://docs.rs/anyhow/latest/anyhow/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/_GoTeoYVlimhjQ6Repw6Iw