「深挖Rust」错误处理 — 6

163 阅读2分钟

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


Trait Objects

在我们着手实现源码之前,让我们仔细看看它的返回: Option<&(dyn Error + 'static)>dyn Error 是一个 Trait Obj。除了它实现了Error的trait外,我们对这个类型一无所知。

Trait Objects,和泛型一样,是Rust中实现多态性的一种方式:调用同一接口的不同实现。泛型在编译时决定类型(静态调度),而 Trait Objects 会有运行时成本(动态调度)。

但是为什么标准库要返回 Trait Objects

它为开发者提供了一种方法来访问当前错误的根本原因,同时保持其不透明性。它不会泄露任何关于底层类型的信息,同时你只能访问 Error trait 所暴露的方法:Debug/Display/source

下面我们给 StoreTokenError 实现 Error

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

impl std::error::Error for StoreTokenError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        // The compiler transparently casts `&sqlx::Error` into a `&dyn Error`
        Some(&self.0)
    }
}

当编写需要处理各种 Error 的代码时,source() 是很有用的:它提供了一种结构化的方式来浏览错误链,而不需要知道你正在处理的具体错误类型是什么。

Error::source

如果我们看一下我们的日志记录,StoreTokenErrorsqlx::Error 之间的因果关系是有点隐式的。我们推断一个是另一个的原因,因为它是它的一部分。

...
 INFO: [HTTP REQUEST - END]
    exception.details= StoreTokenError(
        Database(
            PgDatabaseError {
                severity: Error,
                code: "42703",
                message:
                    "column 'subscription_token' of relation
                     'subscription_tokens' does not exist",
                ...
            }
        )
    )
    exception.message=
        "A database failure was encountered while
         trying to store a subscription token.",
    target=tracing_actix_web::root_span_builder,
    http.status_code=500

下面我们去寻找更明确的打印信息:

//! src/routes/subscriptions.rs

// Notice that we have removed `#[derive(Debug)]`
pub struct StoreTokenError(sqlx::Error);

impl std::fmt::Debug for StoreTokenError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}\nCaused by:\n\t{}", self, self.0)
    }
}

现在的日志记录没有给人留下任何想象的空间:

...
 INFO: [HTTP REQUEST - END]
    exception.details=
        "A database failure was encountered
        while trying to store a subscription token.

        Caused by:
            error returned from database: column 'subscription_token'
            of relation 'subscription_tokens' does not exist"
    exception.message=
        "A database failure was encountered while
         trying to store a subscription token.",
    target=tracing_actix_web::root_span_builder,
    http.status_code=500

exception.details 更容易阅读,并且仍然传达了我们之前所有的相关信息。

使用 source(),我们可以为任何实现Error的类型提供类似的表示:

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

fn error_chain_fmt(
    e: &impl std::error::Error,
    f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
    writeln!(f, "{}\n", e)?;
    let mut current = e.source();
    while let Some(cause) = current {
        writeln!(f, "Caused by:\n\t{}", cause)?;
        current = cause.source();
    }
    Ok(())
}

它遍历了导致我们错误的整个错误链。接下来我们可以改变我们对 StoreTokenErrorDebug 实现,fmt() 中使用它来打印我们的错误链。

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

impl std::fmt::Debug for StoreTokenError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        error_chain_fmt(self, f)
    }
}

结果是相同的 —— 如果我们想要类似的Debug表示,我们可以在处理其他错误时重用 error_chain_fmt