前言
Rust
的文档是偏向于解释型,在示例方面做的并不好,常常是把毫不相关的概念放到了一块,例如 read_to_string 示例,该示例牵扯上了SocketAddr
,对初学者很不友好。你可能已经学习了较久,但依然不知道使用Rust
的正确方式,其中错误处理就是这样一个你必须了解但很难轻易做好的事情,本章我们来谈谈Rust
中的错误处理
正文
处理多种错误类型
让我们再来看下上一篇文章末尾提到的那段无法运行的错误代码:
use std::fs::read_to_string;
fn main() -> Result<(), std::io::Error> {
let html = render_markdown()?;
println!("{}", html);
Ok(())
}
fn render_markdown() -> Result<String, std::io::Error> {
let file = std::env::var("MARKDOWN")?;
let source = read_to_string(file)?;
Ok(markdown::to_html(&source))
}
这段代码的问题在于类型不匹配,main()
要求返回的错误类型是io::Error
,我们使用了两个?
运算符,其中一个返回了不同类型的错误。read_to_string
返回了正确的错误类型Result<String, io::Error>
,但env::var()
返回的错误类型是Result<String, env::VarError>
我们需要一种通用的了类型来匹配上不同种类的错误,如果你是仔细阅读了本教程之前的文章,那么你会知道有两个方法可以解决这个问题:traits
和enums
方式一:Box<dyn Error>
实现对错误的Box
依赖于这些错误对 Error trait 的实现
Rust
必须在编译期知道所有数据的size
,不过一个dyn [trait]
值失去了具体的类型所以Rust
不能知晓它的size
(参见 教程10 )
我们不能简单的返回一个引用,如果该引用所有权限于函数内,则Rust
不会让你简单的返回 ,它的生命周期太短了。Box
技术可以看做是将一个值持久化,延长其生命周期,并保存一个该值的引用以便你能访问到
注意:
Box
的功能不仅限于此,相关文章很多,可自行查阅
现在我们的代码变成这个样子:
use std::{error::Error, fs::read_to_string};
fn main() -> Result<(), Box<dyn Error>> {
let html = render_markdown()?;
println!("{}", html);
Ok(())
}
fn render_markdown() -> Result<String, Box<dyn Error>> {
let file = std::env::var("MARKDOWN")?;
let source = read_to_string(file)?;
Ok(markdown::to_html(&source))
}
现在代码可以正确运行了,不过如果你读了上一篇文章就会知道,Result
不会限制错误类型,如果你代码里的错误没有实现Error trait
,那么依然会出问题
方式二:自定义错误类型
使用dyn [trait]
以失去类型信息作为代价,这是一种让代码正常运行的方案,但不是个长久之计
创建自己的错误类型可以获得更强的掌控力,错误类型可以是struct
或enum
,负责多类型错误的自定义错误类型通常是enum
:
enum MyError {}
一个好的Rust
公民会实现Error trait
,你需要添加如下代码:
impl std::error::Error for MyError {}
上述代码VS Code
会提示错误,使用自动修复
然后会出现更多的提示要求你实现默认方法
好消息是这些代码可以删除,默认的就足够了,之所以报出警告是因为Error trait
需要实现Display
和Debug
:
#[derive(Debug)]
enum MyError {}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error!") // We have nothing useful to display yet.
}
}
注意:
std::fmt::Display
可以直接写成Display
,之所以写成std::fmt::Display
只是为了看上去更加清晰
现在我们的代码整体看上去是这样的:
use std::fs::read_to_string;
fn main() -> Result<(), MyError> {
let html = render_markdown()?;
println!("{}", html);
Ok(())
}
fn render_markdown() -> Result<String, MyError> {
let file = std::env::var("MARKDOWN")?;
let source = read_to_string(file)?;
Ok(markdown::to_html(&source))
}
#[derive(Debug)]
enum MyError {}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error!")
}
}
现在运行代码会爆出两个一样的错误:? coudn't convert the error to MyError
[snipped]
error[E0277]: `?` couldn't convert the error to `MyError`
--> crates/day-14/custom-error-type/src/main.rs:10:39
|
9 | fn render_markdown() -> Result<String, MyError> {
| ----------------------- expected `MyError` because of this
10 | let file = std::env::var("MARKDOWN")?;
| ^ the trait `From<VarError>` is not implemented for `MyError`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= note: required because of the requirements on the impl of `FromResidual<Result<Infallible, VarError>>` for `Result<String, MyError>`
note: required by `from_residual`
[snipped]
我们自定义了一个错误类型,不代表Rust
会知道该如何将其它错误转化成这个类型,错误信息里已经给出了解决方案,我们需要给MyError
类型实现From<env::VarError>
和From<io::Error>
From、 Into、 TryFrom、TryInto traits
From
、 Into
、 TryFrom
和TryInto traits
是奇妙转化的根基,当你查看.into()
时,实际上就是在查看一个或多个上述trait
实现的结果
实现From
提供了反向的Into
,TryFrom
则是反向的TryInto
,Try*
前缀表示转化可能失败,返回一个Result
对于MyError
的实现见下面的代码,留意这里我们增加了MyError
的变体来表示错误类型并且IOError
变体封装了原始的std::io::Error
#[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)
}
}
完整的实现如下,留意在Display
中我们用变体进行了区分:
use std::fs::read_to_string;
fn main() -> Result<(), MyError> {
let html = render_markdown()?;
println!("{}", html);
Ok(())
}
fn render_markdown() -> Result<String, MyError> {
let file = std::env::var("MARKDOWN")?;
let source = read_to_string(file)?;
Ok(markdown::to_html(&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()),
}
}
}
方式三:已有的crate
任何一个Rust
程序员都要处理错误,相应的方法已经有不少先例,没有必要重复造轮子,直接用已有的就很好
thiserror
thiserror
(crates.io) 给你方式二中的全部能力并且用起来更加简洁
use std::fs::read_to_string;
fn main() -> Result<(), MyError> {
let html = render_markdown()?;
println!("{}", html);
Ok(())
}
fn render_markdown() -> Result<String, MyError> {
let file = std::env::var("MARKDOWN")?;
let source = read_to_string(file)?;
Ok(markdown::to_html(&source))
}
#[derive(thiserror::Error, Debug)]
enum MyError {
#[error("Environment variable not found")]
EnvironmentVariableNotFound(#[from] std::env::VarError),
#[error(transparent)]
IOError(#[from] std::io::Error),
}
error-chain
error-chain
已不再维护,但它处理起错误真的很好用
error-chain (crates.io) 给你很多的选择,而且创建错误也十分方便
error_chain::error_chain!{}
这样一行代码会带给你一个错误的struct
、一个错误类型的enum
以及一个关联错误的自定义Result
类型,下面是完整代码:
use std::fs::read_to_string;
error_chain::error_chain! {
foreign_links {
EnvironmentVariableNotFound(::std::env::VarError);
IOError(::std::io::Error);
}
}
fn main() -> Result<()> {
let html = render_markdown()?;
println!("{}", html);
Ok(())
}
fn render_markdown() -> Result<String> {
let file = std::env::var("MARKDOWN")?;
let source = read_to_string(file)?;
Ok(markdown::to_html(&source))
}
anyhow
anyhow 是 thiserror 作者开发的另一款处理错误工具,按照作者本人的说法是,二者之间的区别在于thiserror
更加关心错误的具体类型,是为库类型代码设计;而anyhow
则是为应用型代码设计,不太关心具体的错误类型
相关阅读
相关库
除了正文中介绍到的三个库以外,还有很多很好的库来帮助你简化错误处理
总结
Rust
把错误放在首位,一旦你开始像Rust
那样尊重它们,你就会明白为什么,健壮的错误处理是你可以带回JavaScript
项目的最有价值的东西之一。你将了解如何隔离可能失败的代码,并生成更有意义的错误消息和回退
在代码库中使用thiserror
或error-chain
都是很不错的选择,我在测试和命令行项目里经常使用的是anyhow
。它们都是高品质的可选项,会大大讲话错误的处理