跳转到正文
zeno's blog
返回

Rust 基础:类型获得 Trait 方法的三种方式

专题: Rust 基础

Table of contents

Open Table of contents

问题

为什么有时候 use 一个 trait 就能直接调方法,有时候要 #[derive],有时候要手写 impl

因为获得 trait 方法有三种方式,每种的”谁写了实现”不同。


方式 1:derive 宏——编译器帮你生成实现

use serde::Deserialize;

#[derive(Deserialize)]  // ← 过程宏,编译时自动生成 impl Deserialize for Player
struct Player {
    name: String,
    level: u32,
}

#[derive(Deserialize)] 在编译时读取 Player 的字段定义,自动生成:

// 你看不到这段代码,但编译器生成了它
impl<'de> Deserialize<'de> for Player {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        // 逐字段反序列化 name, level ...
    }
}

不是所有 trait 都有 derive 宏。只有库作者写了过程宏的才行:

#[derive(Debug)]            // 标准库
#[derive(Clone, Copy)]      // 标准库
#[derive(Serialize)]        // serde
#[derive(Deserialize)]      // serde
#[derive(sqlx::FromRow)]    // sqlx
#[derive(thiserror::Error)] // thiserror

方式 2:手写 impl——你自己实现

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        (StatusCode::INTERNAL_SERVER_ERROR, self.0.to_string()).into_response()
    }
}

没有 derive 宏的 trait,或者自动生成的逻辑不符合你的需求时,就手写。

方式 3:blanket impl——库作者已经替所有符合条件的类型实现好了

use anyhow::Context;

let config = std::fs::read_to_string("config.toml")
    .context("failed to read config file")?;
// Result<String, io::Error> 直接就能调 .context()
// 没有 derive,没有手写 impl

因为 anyhow 内部写了:

// anyhow 源码
impl<T, E: std::error::Error> Context<T, E> for Result<T, E> {
    fn context(self, msg: impl Display) -> Result<T, anyhow::Error> { ... }
}

读作:“对于所有 Result<T, E>,只要 E 实现了 Error,就自动拥有 .context() 方法”

这叫 blanket implementation(覆盖性实现)。库作者一次性给所有满足条件的类型实现了这个 trait。你不需要做任何事,条件满足就自动拥有。

use anyhow::Context 的作用不是”给你的类型添加能力”,而是让编译器看到这个 trait 存在。实现早就写好了,use 只是让它在当前作用域可见。

标准库中的 blanket impl 例子

// 任何实现了 Display 的类型,自动获得 .to_string()
impl<T: Display> ToString for T {
    fn to_string(&self) -> String {
        format!("{}", self)
    }
}

// 所以你不需要为自己的类型手动实现 ToString
struct PlayerId(u32);

impl Display for PlayerId {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "player_{}", self.0)
    }
}

let id = PlayerId(42);
id.to_string();  // "player_42" — 你只实现了 Display,自动获得了 ToString
// 任何实现了 Into<U> 的类型... 不对,反过来:
// 任何 U 实现了 From<T>,T 就自动获得 Into<U>
impl<T, U> Into<U> for T where U: From<T> {
    fn into(self) -> U {
        U::from(self)
    }
}

// 所以你只需要实现 From,Into 自动获得
impl From<u32> for PlayerId {
    fn from(id: u32) -> Self { PlayerId(id) }
}

let id: PlayerId = 42_u32.into();  // .into() 是自动获得的

对比总结

方式              谁写了 impl          你要做什么             典型例子
──────────────────────────────────────────────────────────────────────────
derive 宏        编译器在编译时生成     加 #[derive(Xxx)]     Serialize, Debug, Clone
手写 impl        你自己               写 impl Xxx for T     IntoResponse, FromRequest
blanket impl     库作者已经写好        只需 use 引入 trait    anyhow::Context, ToString
判断流程:

  这个 trait 有 derive 宏吗?
    ├── 有 → #[derive(Xxx)] 搞定
    └── 没有
          ├── 库里有 blanket impl 覆盖了我的类型吗?
          │     ├── 有 → use 引入就能用
          │     └── 没有 → 手写 impl
          └── 不确定 → 试试 use 后直接调,编译器会告诉你

分享这篇文章:

上一篇
Rust 基础:类型转换 as、From/Into 与 Turbofish
下一篇
Rust 基础:Serde 序列化框架