所有权与不变量

我们修改了 insert_subscriber 的签名,但尚未修改主体以符合新的要求——现在就修改吧。

//! src/routes/subscriptions.rs

// [...]

#[tracing::instrument(/*[...]*/)]
pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse {
    let new_subscriber = NewSubscriber {
        email: form.0.email,
        name: SubscriberName::parse(form.0.name),
    };
    match insert_subscriber(&pool, &new_subscriber).await {
        Ok(_) => HttpResponse::Ok().finish(),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}


#[tracing::instrument(
    name = "Saving new subscriber details in the database",
    skip(new_subscriber, pool)
)]
pub async fn insert_subscriber(pool: &PgPool, new_subscriber: &NewSubscriber) -> Result<(), sqlx::Error> {
    sqlx::query!(
        r#"
        INSERT INTO subscriptions (id, email, name, subscribed_at)
        VALUES ($1, $2, $3, $4)
        "#,
        Uuid::new_v4(),
        new_subscriber.email,
        new_subscriber.name,
        Utc::now()
    )
    .execute(pool)
    .await
    .inspect_err(|e| {
        tracing::error!("Failed to execute query: {:?}", e);
    })?;

    Ok(())
}

我们很接近了, 但 cargo check 还没办法通过:

error[E0308]: mismatched types
  --> src/routes/subscriptions.rs:46:9
   |
46 |         new_subscriber.name,
   |         ^^^^^^^^^^^^^^
   |         |
   |         expected `&str`, found `SubscriberName`
   |         expected due to the type of this binding

error[E0277]: the trait bound `SubscriberName: sqlx::Encode<'_, Postgres>` is no
t satisfied

这里有一个问题: 我们没有任何方法可以真正访问封装在 SubscriberName 中的字符串值!

我们可以将 SubscriberName 的定义从 SubscriberName(String) 改为 SubscriberName(pubString),但这样一来,我们就会失去前两节中提到的所有好处:

  • 其他开发者可以绕过解析,使用任意字符串构建 SubscriberName
let liar = SubscriberName("".to_string());
  • 其他开发者可能仍然会选择使用 parse 来构建 SubscriberName,但他们随后可以选择将内部值更改为不再满足我们关心的约束的值
let mut started_well = SubscriberName::parse("A valid name".to_string());
started_well.0 = "".to_string();

我们可以做得更好——这正是 Rust 所有权系统的优势所在! 给定结构体中的某个字段,我们可以选择:

  • 通过值暴露它,使用消耗结构体本身:
impl SubscriberName {
    pub fn inner(self) -> String {
        // The caller gets the inner string,
        // but they do not have a SubscriberName anymore!
        // That's because `inner` takes `self` by value,
        // consuming it according to move semantics
        self.0
    }
}

- 暴露可变引用

```rs
impl SubscriberName {
    pub fn inner_mut(&mut self) -> &mut str {
        // The caller gets a mutable reference to the inner string.
        // This allows them to perform *arbitrary* changes to
        // value itself, potentially breaking our invariants!
        &mut self.0
    }
}

暴露引用

impl SubscriberName {
    pub fn inner_ref(&self) -> &str {
        // The caller gets a shared reference to the inner string.
        // This gives the caller **read-only** access,
        // they have no way to compromise our invariants!
        &self.0
    }
}

inner_mut 并非我们想要的效果——失去对不变量的控制,相当于使用 SubscriberName(pub String)。

inner 和 inner_ref 都适用,但 inner_ref 更好地传达了我们的意图:

让调用者有机会读取值,但无法对其进行修改。

让我们将 inner_ref 添加到 SubscriberName 中——然后我们可以修改 insert_subscriber 来使用它:

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

#[tracing::instrument(
    name = "Saving new subscriber details in the database",
    skip(new_subscriber, pool)
)]
pub async fn insert_subscriber(pool: &PgPool, new_subscriber: &NewSubscriber) -> Result<(), sqlx::Error> {
    sqlx::query!(
        r#"
        INSERT INTO subscriptions (id, email, name, subscribed_at)
        VALUES ($1, $2, $3, $4)
        "#,
        Uuid::new_v4(),
        new_subscriber.email,
        // Using `inner_ref`!
        new_subscriber.name.inner_ref(),
        Utc::now()
    )
    .execute(pool)
    .await
    .inspect_err(|e| {
        tracing::error!("Failed to execute query: {:?}", e);
    })?;

    Ok(())
}

轰隆,编译成功了!

AsRef

虽然我们的 inner_ref 方法完成了任务,但我必须指出,Rust 的标准库公开了一个专为此类用法而设计的特性——AsRef

其定义非常简洁:

pub trait AsRef<T>
where
    T: ?Sized,
{
    // Required method
    fn as_ref(&self) -> &T;
}

什么时候应该为某个类型实现 AsRef<T>? 当该类型与 T 足够相似,以至于我们可以使用 &self 来获取 T 自身的引用时!

这听起来是不是太抽象了?再看看 inner_ref 的签名:它本质上就是为 SubscriberName 实现的 AsRef<str>!

AsRef 可以用来提升用户体验——让我们考虑一个具有以下签名的函数:

pub fn do_something_with_a_string_slice(s: &str) {
    // [...]
}

没什么太复杂的,但你可能需要花点时间弄清楚 SubscriberName 是否能提供 &str ,以及如何提供,尤其是当该类型来自第三方库时。

我们可以通过更改 do_something_with_a_string_slice 的签名来让体验更加无缝:

// We are constraining T to implement the AsRef<str> trait
// using a trait bound - `T: AsRef<str>`
pub fn do_something_with_a_string_slice<T: AsRef<str>>(s: T) {
    let s = s.as_ref();
    // [...]
}

我们现在可以写

let name = SubscriberName::parse("A valid name".to_string());
do_something_with_a_string_slice(name)

它会立即编译通过(假设 SubscriberName 实现了 AsRef<str> 接口)。

这种模式被广泛使用,例如,在 Rust 标准库 std::fs 中的文件系统模块中。像 create_dir 这样的函数接受一个 P 类型的参数,并强制要求实现 AsRef<Path> 接口,而不是强迫用户理解如何将 String 转换为 Path,或者如何将 PathBuf 转换为 Path,或者 OsString,等等...你懂的。

该标准库中还有其他一些像 AsRef 这样的小转换特性——它们为整个生态系统提供了一个共享的接口,以便围绕它们进行标准化。为你的类型实现这些特性,可以立即解锁大量通过现有的 crate 中的泛型类型公开的功能。

我们稍后会介绍其他一些转换特性(例如 From/Into、TryFrom/TryInto)。

让我们删除 inner_ref 并为 SubscriberName 实现 AsRef<str>:

//! src/domain.rs
// [...]
impl AsRef<str> for SubscriberName {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

我们还需要修改 insert_subscriber:

//! src/routes/subscriptions.rs
// [...]
pub async fn insert_subscriber(pool: &PgPool, new_subscriber: &NewSubscriber) -> Result<(), sqlx::Error> {
    sqlx::query!(
        r#"
        INSERT INTO subscriptions (id, email, name, subscribed_at)
        VALUES ($1, $2, $3, $4)
        "#,
        Uuid::new_v4(),
        new_subscriber.email,
        // Using `as_ref` now!
        new_subscriber.name.as_ref(),
        Utc::now()
    )
    .execute(pool)
    .await
    .inspect_err(|e| {
        tracing::error!("Failed to execute query: {:?}", e);
    })?;

    Ok(())
}

该项目可以编译通过...