将错误作为值 - Result

Rust 的主要错误处理机制建立在 Result 类型之上:

enum Result<T, E> {
   Ok(T),
   Err(E),
}

Result 用作易出错操作的返回类型: 如果操作成功,则返回 Ok(T); 如果失败,则返回 Err(E)

我们实际上已经使用过 Result 了,尽管当时我们并没有停下来讨论它的细微差别。

让我们再看一下 insert_subscriber 的签名:

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

pub async fn insert_subscriber(
    pool: &PgPool,
    new_subscriber: &NewSubscriber,
) -> Result<(), sqlx::Error> {
    // [...]
}

它告诉我们,在数据库中插入订阅者是一个容易出错的操作——如果一切按计划进行,我们不会收到任何返回(() - 单位类型),如果出现问题,我们将收到一个 sqlx::Error,其中包含出错的详细信息(例如连接问题)。

将错误作为值,与 Rust 的枚举相结合,是构建健壮错误处理机制的绝佳基石。

如果你之前使用的语言支持基于异常的错误处理,那么这可能会带来翻天覆地的变化:我们需要了解的关于函数故障模式的一切都包含在它的签名中。

你无需深入研究依赖项的文档来了解某个函数可能抛出的异常(假设它事先已记录在案!)。

你不会在运行时对另一个未记录的异常类型感到惊讶。你不必“以防万一”地插入一个 catch-all 语句。

我们将在这里介绍基础知识,并将更详细的细节(Error trait)留到下一章。

parse 中加入 Result 返回值

让我们重构一下 SubscriberName::parse 函数,让它返回一个 Result,而不是因为输入无效而 panic。

我们先修改一下签名,但不修改函数体:

//! src/domain.rs
// [...]
impl SubscriberName {
    pub fn parse(s: String) -> Result<SubscriberName, ???> {
        // [...]
    }
}

我们应该使用什么类型作为 Result 的 Err 变量?

最简单的选择是字符串 - 失败时我们只会返回一条错误消息。

impl SubscriberName {
    pub fn parse(s: String) -> Result<SubscriberName, String> {
        // [...]
    }
}

运行 cargo check 会产生两个错误:

error[E0308]: mismatched types
  --> src/routes/subscriptions.rs:25:15
   |
25 |         name: SubscriberName::parse(form.0.name),
   |               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `SubscriberName`,
 found `Result<SubscriberName, String>`
   |
   = note: expected struct `SubscriberName`
                found enum `Result<SubscriberName, std::string::String>`
help: consider using `Result::expect` to unwrap the `Result<SubscriberName, std:
:string::String>` value, panicking if the value is a `Result::Err`
   |
25 |         name: SubscriberName::parse(form.0.name).expect("REASON"),
   |                                                 +++++++++++++++++

error[E0308]: mismatched types
  --> src/domain.rs:20:13
   |
11 |     pub fn parse(s: String) -> Result<SubscriberName, String> {
   |                                ------------------------------ expected `Res
ult<SubscriberName, std::string::String>` because of return type
...
20 |             Self(s)
   |             ^^^^^^^ expected `Result<SubscriberName, String>`, found `Subsc
riberName`
   |
   = note: expected enum `Result<SubscriberName, std::string::String>`
            found struct `SubscriberName`
help: try wrapping the expression in `Ok`
   |
20 |             Ok(Self(s))
   |             +++       +

For more information about this error, try `rustc --explain E0308`.
error: could not compile `zero2prod` (lib) due to 2 previous errors

让我们关注第二个错误:在 parse 结束时,我们无法返回一个 SubscriberName 的裸实例——我们需要在两个 Result 变体中选择一个。

编译器理解了这个问题,并建议了正确的修改:使用 Ok(Self(s)) 而不是 Self(s)。

让我们遵循它的建议:

//! src/domain.rs
// [...]
impl SubscriberName {
    pub fn parse(s: String) -> Result<SubscriberName, String> {
        // [...]

        if is_empty_or_whitespace || is_too_long || contains_forbidden_characters {
            panic!("{s} is not a valid subscriber name");
        } else {
            Ok(Self(s))
        }
    }
}

cargo check 现在应该返回一个错误:

error[E0308]: mismatched types
  --> src/routes/subscriptions.rs:25:15
   |
25 |         name: SubscriberName::parse(form.0.name),
   |               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `SubscriberName`,
 found `Result<SubscriberName, String>`
   |
   = note: expected struct `SubscriberName`
                found enum `Result<SubscriberName, std::string::String>`
help: consider using `Result::expect` to unwrap the `Result<SubscriberName, std:
:string::String>` value, panicking if the value is a `Result::Err`
   |
25 |         name: SubscriberName::parse(form.0.name).expect("REASON"),
   |                                                 +++++++++++++++++

For more information about this error, try `rustc --explain E0308`.
error: could not compile `zero2prod` (lib) due to 1 previous error

它抱怨我们在 subscribe 中调用 parse 方法: 当 parse 返回一个 SubscriberName 时,将其输出直接赋值给 Subscriber.name 是完全没问题的。

现在我们返回一个 Result —— Rust 的类型系统迫使我们处理这条不愉快的路径。我们不能假装它不会发生。

不过,我们先避免一下子讲太多——为了让项目尽快再次编译,我们暂时只在验证失败时触发 panic:

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

pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse {
    let new_subscriber = NewSubscriber {
        email: form.0.email,
        // Notice the usage of `expect` to specify a meaningful panic message
        name: SubscriberName::parcse(form.0.name).expect("Name validation failed."),
    };
    // [...]
}

cargo check 现在应该很顺利了。

该进行测试了!