写给前端看的Rust教程(14)Errors

1,501 阅读6分钟

原文:24 days from node.js to Rust

前言

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>

我们需要一种通用的了类型来匹配上不同种类的错误,如果你是仔细阅读了本教程之前的文章,那么你会知道有两个方法可以解决这个问题:traitsenums

方式一: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]以失去类型信息作为代价,这是一种让代码正常运行的方案,但不是个长久之计

创建自己的错误类型可以获得更强的掌控力,错误类型可以是structenum,负责多类型错误的自定义错误类型通常是enum

enum MyError {}

一个好的Rust公民会实现Error trait,你需要添加如下代码:

impl std::error::Error for MyError {}

上述代码VS Code会提示错误,使用自动修复

image.png

然后会出现更多的提示要求你实现默认方法

image.png

好消息是这些代码可以删除,默认的就足够了,之所以报出警告是因为Error trait需要实现DisplayDebug

#[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、 TryFromTryInto traits是奇妙转化的根基,当你查看.into()时,实际上就是在查看一个或多个上述trait实现的结果

实现From提供了反向的IntoTryFrom则是反向的TryIntoTry*前缀表示转化可能失败,返回一个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

anyhowthiserror 作者开发的另一款处理错误工具,按照作者本人的说法是,二者之间的区别在于thiserror更加关心错误的具体类型,是为库类型代码设计;而anyhow则是为应用型代码设计,不太关心具体的错误类型

相关阅读

相关库

除了正文中介绍到的三个库以外,还有很多很好的库来帮助你简化错误处理

总结

Rust把错误放在首位,一旦你开始像Rust那样尊重它们,你就会明白为什么,健壮的错误处理是你可以带回JavaScript项目的最有价值的东西之一。你将了解如何隔离可能失败的代码,并生成更有意义的错误消息和回退

在代码库中使用thiserrorerror-chain都是很不错的选择,我在测试和命令行项目里经常使用的是anyhow。它们都是高品质的可选项,会大大讲话错误的处理