「深挖Rust」错误处理 — 9

259 阅读3分钟

「这是我参与2022首次更文挑战的第 10 天,活动详情查看:2022首次更文挑战」。


用 thiserror 替代模板

前面我们花了大约90行代码来实现 SubscriberError 和围绕它的所有机制,以实现所需的行为并在我们的日志中获得有用的诊断。

这是大量的代码,还有大量的模板(例如,源代码或From实现)。我们能做得更好吗?

我不确定我们能不能少写一些代码,但我们可以找到一个不同的方法:我们可以用一个宏来生成所有的模板。

碰巧的是,在生态系统中已经有一个很好的 crate 来实现这个目的:thiserror。让我们把它添加到我们的依赖项中。

#! Cargo.toml

[dependencies]
# [...]
thiserror = "1"

它提供了一个派生宏来生成我们刚刚手工编写的大部分代码。让我们看看它的实际应用:

//! src/routes/subscriptions.rs
// [...]

#[derive(thiserror::Error)]
pub enum SubscribeError {
    #[error("{0}")]
    ValidationError(String),
    #[error("Failed to acquire a Postgres connection from the pool")]
    PoolError(#[source] sqlx::Error),
    #[error("Failed to insert new subscriber in the database.")]
    InsertSubscriberError(#[source] sqlx::Error),
    #[error("Failed to store the confirmation token for a new subscriber.")]
    StoreTokenError(#[from] StoreTokenError),
    #[error("Failed to commit SQL transaction to store a new subscriber.")]
    TransactionCommitError(#[source] sqlx::Error),
    #[error("Failed to send a confirmation email.")]
    SendEmailError(#[from] reqwest::Error),
}

// We are still using a bespoke implementation of `Debug`
// to get a nice report using the error source chain
impl std::fmt::Debug for SubscribeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        error_chain_fmt(self, f)
    }
}

pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
    // We no longer have `#[from]` for `ValidationError`, so we need to
    // map the error explicitly
    let new_subscriber = form.0.try_into().map_err(SubscribeError::ValidationError)?;
    // [...]
}

我们把它削减到了21行--还不错!让我们来分析一下上面正在发生什么。

thiserror::Error 是一个通过 #[derive(/* */)] 属性使用的程序性宏。

我们以前也见过并使用过这些。例如:

  • #[derive(Debug)]
  • #[derive(serde::Serialize)]

该宏在编译时接收 SubscribeErrorToken 作为输入,并返回另一个 Token 流作为输出即:它生成新的Rust代(derive的部分会被加上),然后被编译成最终的二进制文件。

#[derive(thiserror::Error)] 的上下文中,我们可以利用其他属性来实现之前的模版:

  • #[error(/* */)] 定义了它所应用的枚举变量的显示表示。例如,当对SubscribeError::SendEmailError 的case调用时,Display将返回 Failed to send a confirmation email。你可以在最终显示中插值(插入String) → 例如,ValidationError上面的 #[error("{0}")] 中的{0}是指被包裹的String字段,模仿了访问元组结构上的字段的语法(即self.0)
  • #[source] 用来表示在 Error::source 中应该作为根本原因返回的东西
  • #[from] 为它所应用的类型自动派生出From的实现,使之可以转换为顶层的错误类型(例如,SubscribeError {/* */}的 impl From)。用 #[from] 注释的字段也被用作错误源,使我们不必在同一个字段上使用两个注释(例如,#[source] #[from] reqwest::Error,只需要标注一个即可)

我想提醒你注意一个小细节:我们没有为 ValidationError 变量使用 #[from]/#[source]

这是因为 String 没有实现 Error Trait,因此它不能在 Error::source 中返回。这与我们之前手动实现 Error::source 时遇到的限制相同,这导致我们在 ValidationError 情况下返回 None