问题背景 我们在Rust错误处理中常遇到的情况是,某个函数需要配合?
抛出多种错误
1 2 3 4 5 6 7 8 fn g1 () -> Result <&'static str , std::io::Error> { Ok ("hello 1" ) } fn g2 () -> Result <&'static str , std::net::AddrParseError> { Ok ("hello 2" ) }
此时我们在main中想处理这几种错误怎么做呢?
1. trait object 一种常见的方式就是用trait object包装std::error::Error
1 2 3 4 5 fn main () -> Result <(), Box <dyn std::error::Error>> { g1 ()?; g2 ()?; Ok (()) }
这种方式对于一些轻量场景工作的很好,比如外层只需要打印错误信息,并不关心其他细节。
1 2 3 4 5 6 7 8 9 10 11 fn main () -> Result <(), Box <dyn std::error::Error>> { let r = handle (); println! ("{:?}" ,r); Ok (()) } fn handle () -> Result <(), Box <dyn std::error::Error>> { g1 ()?; g2 ()?; Ok (()) }
然而这种方式有几个问题:
如果我们想Error情况分别处理,那可能会遇到问题。
虽然trait object支持通过downcast做类型转换,但是必须预先知道错误类型。
1 2 3 4 5 6 7 8 9 10 fn main () -> Result <(), Box <dyn std::error::Error>> { let r = handle (); match r { Ok (_) => {}, Err (e) => { let e0 = e.downcast::<std::io::Error>(); }, } Ok (()) }
另一个麻烦的问题是,这种方式要求Result<T,E>
必须实现了std::error::Error
,实际上 Result<T,E>
并没有限制E必须实现std::error::Error
。
2. 自己实现From 自己实现From,将多种Error转为一个Error枚举,?
会通过From进行隐式转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #[derive(Debug)] enum MyAppError { IO (std::io::Error), Net (std::net::AddrParseError), } impl Display for MyAppError { fn fmt (&self , f: &mut std::fmt::Formatter<'_ >) -> std::fmt::Result { write! (f, "MyAppError:" )?; match self { MyAppError::IO (e) => write! (f, "IO: {}" , e)?, MyAppError::Net (e) => write! (f, "Net: {}" , e)?, }; Ok (()) } } impl From <std::io::Error> for MyAppError { fn from (e: std::io::Error) -> MyAppError { MyAppError::IO (e) } } impl From <std::net::AddrParseError> for MyAppError { fn from (e: std::net::AddrParseError) -> MyAppError { MyAppError::Net (e) } } fn main () -> Result <(), MyAppError> { if let Err (e) = handle () { match e { MyAppError::IO (e) => todo!(), MyAppError::Net (e) => todo!(), } } Ok (()) } fn handle () -> Result <(), MyAppError> { g1 ()?; g2 ()?; Ok (()) }
这种方式应该说失去了一些灵活性,也必须事先枚举出所有可能的错误。
3. thiserror 这个crates能简化一些上面的工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 fn g1 () -> Result <String , MyAppError> { let bytes = vec! [0 , 159 ]; Ok (String ::from_utf8 (bytes)?) } fn g2 () -> Result <&'static str , MyAppError> { return Err (MyAppError::IO (std::io::Error::new ( std::io::ErrorKind::Other, "test" , ))); } #[derive(Debug, thiserror::Error)] enum MyAppError { #[error("MyApp IO Error {0} " )] IO (#[from] std::io::Error), #[error("MyApp FromUtf8Error Error {0}" )] Utf8 (#[from] FromUtf8Error), #[error("invalid header (expected {expected:?}, found {found:?})" )] InvalidHeader { expected: String , found: String , }, } fn main () -> Result <(), MyAppError> { if let Err (e) = handle () { println! ("{}" ,e); match e { MyAppError::IO (e) => println! ("catch IO Error=> {}" , e), MyAppError::Utf8 (e) => println! ("catch Utf8 => {}" , e) } } Ok (()) } fn handle () -> Result <(), MyAppError> { g1 ()?; g2 ()?; Ok (()) }
在thiserror中 #[Error]
派生宏为struct
实现了,std::error::Error
#[error("MyApp IO Error {0} ")]
则为其实现了Display其中可以引用变量display。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #[allow(unused_qualifications)] impl std ::fmt::Display for MyAppError { fn fmt (&self , __formatter: &mut std::fmt::Formatter) -> std::fmt::Result { #[allow(unused_imports)] use thiserror::__private::{DisplayAsDisplay, PathAsDisplay}; #[allow(unused_variables, deprecated, clippy::used_underscore_binding)] match self { MyAppError::IO (_0) => write! ( __formatter, "MyApp IO Error {field__0} " , field__0 = _0.as_display () ), MyAppError::Utf8 (_0) => write! ( __formatter, "MyApp FromUtf8Error Error {field__0}" , field__0 = _0.as_display () ), } } }
#[from]
宏则实现了原错误类型到我们自定义的错误类型之间的转换。因此我们可以直接使用?
。
4. anyhow 所有实现了std::Error
trait的结构体,统一转换成它定义的anyhow::Error
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 use anyhow::Result ;use std::{fs::read_to_string};use anyhow::Context;fn g1 () -> Result <String > { let bytes = vec! [0 , 159 ]; Ok (String ::from_utf8 (bytes)?) } fn g2 () -> Result <String > { let file = std::env::var ("MARKDOWN" )?; Ok (read_to_string (file)?) } fn main () -> Result <()> { handle ()?; Ok (()) } fn handle () -> Result <()> { g1 ().with_context (||format! ("fail to parse" ))?; g2 ()?; Ok (()) }
with_context
是可以添加上下文。
1 2 3 4 5 6 7 8 9 10 11 Error: fail to Caused by: invalid utf-8 sequence of 1 bytes from index 1 Stack backtrace: 0: std::backtrace_rs::backtrace::libunwind::trace at /rustc/b8b5caee04116c7383eb1c6470fcf15c437a60d4/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5 1: std::backtrace_rs::backtrace::trace_unsynchronized at /rustc/b8b5caee04116c7383eb1c6470fcf15c437a60d4/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
可以基于字符串生成anyhow::Error
1 2 3 4 fn g2 () -> Result <String > { let file = std::env::var ("MARKDOWN" )?; Err (anyhow::anyhow!("fail to read file" )) }
实践经验表明:在库开发中不应使用anyhow,这是因为anyhow暴露出的错误仍抹去了原信息,对于希望获取原Error的用户则需要使用downcast转换,不是很好的设计。库开发使用thiserror明确提供enum是比较好的设计。 在应用开发中则可以使用anyhow和thiserror结合。