rust 笔记 高级错误处理(一)https://developer.aliyun.com/article/1392099
上面的例子很简单,我们定义了一个错误类型,当为它派生了 Debug 特征,同时手动实现了 Display 特征后,该错误类型就可以作为 Err来使用了。
事实上,实现 Debug 和 Display 特征并不是作为 Err 使用的必要条件,大家可以把这两个特征实现和相应使用去除,然后看看代码会否报错。既然如此,我们为何要为自定义类型实现这两个特征呢?原因有二:
错误得打印输出后,才能有实际用处,而打印输出就需要实现这两个特征
可以将自定义错误转换成 Box
特征对象,在后面的归一化不同错误类型部分.
现在再来定义一个具有错误码和信息的错误:
use std::fmt; struct AppError { code: usize, message: String, } // 根据错误码显示不同的错误信息 impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let err_msg = match self.code { 404 => "Sorry, Can not find the Page!", _ => "Sorry, something is wrong! Please Try Again!", }; // 向一个缓冲区里写格式化的数据。 // write!将格式化的内容写入到指定的输出流中,例如write!(std::io::stdout(), "hello {}!", "world")将在标准输出流中打印出hello world!。 // print!也是对输出内容进行格式化,但是输出的目标是标准输出流 write!(f, "{}", err_msg) } } impl fmt::Debug for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "AppError {{ code: {}, message: {} }}", self.code, self.message ) } } // 测试错误方法 fn produce_error() -> Result<(), AppError> { Err(AppError { code: 404, message: String::from("Page not found"), }) } fn main() { match produce_error() { Err(e) => eprintln!("{}", e), // 抱歉,未找到指定的页面! _ => println!("No error"), } eprintln!("{:?}", produce_error()); // Err(AppError { code: 404, message: Page not found }) eprintln!("{:#?}", produce_error()); // Err( // AppError { code: 404, message: Page not found } // ) }
在本例中,我们除了增加了错误码和消息外,还手动实现了 Debug 特征,原因在于,我们希望能自定义 Debug 的输出内容,而不是使用派生后系统提供的默认输出形式。
错误转换 From 特征
标准库、三方库、本地库,各有各的精彩,各也有各的错误。那么问题就来了,我们该如何将其它的错误类型转换成自定义的错误类型?总不能神鬼牛魔,同台共舞吧。
好在 Rust 为我们提供了 std::convert::From 特征:
pub trait From<T>: Sized { fn from(_: T) -> Self; }
大家都使用过 String::from 函数吧?它可以通过 &str 来创建一个 String,其实该函数就是 From 特征提供的
下面一起来看看如何为自定义类型实现 From 特征:
use std::fs::File; use std::io; #[derive(Debug)] struct AppError { kind: String, // 错误类型 message: String, // 错误信息 } // 为 AppError 实现 std::convert::From 特征,由于 From 包含在 std::prelude 中,因此可以直接简化引入。 // 实现 From<io::Error> 意味着我们可以将 io::Error 错误转换成自定义的 AppError 错误 impl From<io::Error> for AppError { fn from(error: io::Error) -> Self { AppError { kind: String::from("io"), message: error.to_string(), } } } fn main() -> Result<(), AppError> { let _file = File::open("nonexistent_file.txt")?; Ok(()) }
Error: AppError { kind: "io", message: "No such file or directory (os error 2)" }
上面的代码中除了实现 From 外,还有一点特别重要,那就是 ? 可以将错误进行隐式的强制转换:File::open 返回的是 std::io::Error, 我们并没有进行任何显式的转换,它就能自动变成 AppError ,这就是 ? 的强大之处!当然实现隐式转换的前提是实现了对应的From;
再来看看多个不同的错误转换成 AppError 的实现:
use std::fs::File; use std::io::{self, Read}; use std::num; #[derive(Debug)] struct AppError { kind: String, message: String, } impl From<io::Error> for AppError { fn from(error: io::Error) -> Self { AppError { kind: String::from("io"), message: error.to_string(), } } } impl From<num::ParseIntError> for AppError { fn from(error: num::ParseIntError) -> Self { AppError { kind: String::from("parse"), message: error.to_string(), } } } fn main() -> Result<(), AppError> { let mut file = File::open("hello_world.txt")?; let mut content = String::new(); file.read_to_string(&mut content)?; let _number: usize; _number = content.parse()?; Ok(()) }
// 01. 若 hello_world.txt 文件不存在 Error: AppError { kind: "io", message: "No such file or directory (os error 2)" } // 02. 若用户没有相关的权限访问 hello_world.txt Error: AppError { kind: "io", message: "Permission denied (os error 13)" } // 03. 若 hello_world.txt 包含有非数字的内容,例如 Hello, world! Error: AppError { kind: "parse", message: "invalid digit found in string" }
归一化不同的错误类型
在实际项目中,我们往往会为不同的错误定义不同的类型,这样做非常好,但是如果你要在一个函数中返回不同的错误呢?例如:
use std::fs::read_to_string; fn main() -> Result<(), std::io::Error> { let html = render()?; println!("{}", html); Ok(()) } fn render() -> Result<String, std::io::Error> { let file = std::env::var("MARKDOWN")?; let source = read_to_string(file)?; Ok(source) }
上面的代码会报错,原因在于 render 函数中的两个 ? 返回的实际上是不同的错误:env::var() 返回的是 std::env::VarError,而 read_to_string 返回的是 std::io::Error。
为了满足 render 函数的签名,我们就需要将 env::VarError 和 io::Error 归一化为同一种错误类型。要实现这个目的有三种方式:
- 使用特征对象 Box
- 自定义错误类型
- 使用 thiserror
下面依次来看看相关的解决方式。
Box
use std::fs::read_to_string; use std::error::Error; fn main() -> Result<(), Box<dyn Error>> { let html = render()?; println!("{}", html); Ok(()) } fn render() -> Result<String, Box<dyn Error>> { let file = std::env::var("MARKDOWN")?; let source = read_to_string(file)?; Ok(source) }
这个方法很简单,在绝大多数场景中,性能也非常够用,但是有一个问题:Result 实际上不会限制错误的类型,也就是一个类型就算不实现 Error 特征,它依然可以在 Result 中作为 E 来使用,此时这种特征对象的解决方案就无能为力了。
自定义错误类型
与特征对象相比,自定义错误类型麻烦归麻烦,但是它非常灵活,因此也不具有上面的类似限制:
use std::fs::read_to_string; fn main() -> Result<(), MyError> { let html = render()?; println!("{}", html); Ok(()) } fn render() -> Result<String, MyError> { let file = std::env::var("MARKDOWN")?; let source = read_to_string(file)?; Ok(source) } #[derive(Debug)] enum MyError { EnvironmentVariableNotFound, IOError(std::io::Error), } impl From<std::env::VarError> for MyError { fn from(_: std::env::VarError) -> Self { Self::EnvironmentVariableNotFound } } impl From<std::io::Error> for MyError { fn from(value: std::io::Error) -> Self { Self::IOError(value) } } impl std::error::Error for MyError {} impl std::fmt::Display for MyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MyError::EnvironmentVariableNotFound => write!(f, "Environment variable not found"), MyError::IOError(err) => write!(f, "IO Error: {}", err.to_string()), } } }
上面代码中有一行值得注意:impl std::error::Error for MyError {} ,只有为自定义错误类型实现 Error 特征后,才能转换成相应的特征对象。
简化错误处理
对于开发者而言,错误处理是代码中打交道最多的部分之一,因此选择一把趁手的武器也很重要,它可以帮助我们节省大量的时间和精力,好钢应该用在代码逻辑而不是冗长的错误处理上。
thiserror
thiserror
可以帮助我们简化上面的第二种解决方案:
use std::fs::read_to_string; fn main() -> Result<(), MyError> { let html = render()?; println!("{}", html); Ok(()) } fn render() -> Result<String, MyError> { let file = std::env::var("MARKDOWN")?; let source = read_to_string(file)?; Ok(source) } #[derive(thiserror::Error, Debug)] enum MyError { #[error("Environment variable not found")] EnvironmentVariableNotFound(#[from] std::env::VarError), #[error(transparent)] IOError(#[from] std::io::Error), }
anyhow
anyhow 和 thiserror 是同一个作者开发的,这里是作者关于 anyhow 和 thiserror 的原话:
如果你想要设计自己的错误类型,同时给调用者提供具体的信息时,就使用 thiserror,例如当你在开发一个三方库代码时。如果你只想要简单,就使用 anyhow,例如在自己的应用服务中。
use std::fs::read_to_string; use anyhow::Result; fn main() -> Result<()> { let html = render()?; println!("{}", html); Ok(()) } fn render() -> Result<String> { let file = std::env::var("MARKDOWN")?; let source = read_to_string(file)?; Ok(source) }
关于如何选用 thiserror 和 anyhow 只需要遵循一个原则即可:是否关注自定义错误消息,关注则使用 thiserror(常见业务代码),否则使用 anyhow(编写第三方库代码).