跳转到正文
zeno's blog
返回

Rust 泛型(二):结构体与 Trait 逐步理解

专题: Rust 基础

Table of contents

Open Table of contents

第一步:一个普通 trait + 一个普通结构体

trait Greet {
    fn hello(&self) -> String;
}

struct Dog {
    name: String,
}

impl Greet for Dog {
    fn hello(&self) -> String {
        format!("Woof! I'm {}", self.name)
    }
}

let dog = Dog { name: "Rex".into() };
dog.hello();  // "Woof! I'm Rex"

没有泛型,一切都是具体类型。

第二步:结构体加上泛型

struct Container<T> {
    value: T,
}

Container<T> 不是一个类型——它是一个模具。给它一个具体类型,才能压出一个具体类型:

Container<i32>    → 一个装 i32 的容器(具体类型)
Container<String> → 一个装 String 的容器(另一个具体类型)
Container<Dog>    → 一个装 Dog 的容器(又一个具体类型)

它们之间没有关系,就像”铁盒子”和”纸盒子”都是盒子但是不同的东西。

第三步:为泛型结构体实现普通 trait

impl<T: Display> Greet for Container<T> {
    fn hello(&self) -> String {
        format!("I'm a container holding: {}", self.value)
    }
}

Container { value: 42 }.hello();       // OK,i32 能 Display
Container { value: "hi" }.hello();     // OK,&str 能 Display
Container { value: vec![1] }.hello();  // 编译错误!Vec 不能 Display

impl<T: Display> 读作:“对于任何实现了 Display 的类型 T”

第四步:Trait 也加上泛型

trait Extract<S> {
    fn extract(source: &S) -> Self;
}

Extract<S> 也是一个模具。S 不同,trait 就不同:

Extract<HttpRequest>  → "我能从 HTTP 请求中提取出来"
Extract<String>       → "我能从字符串中提取出来"
Extract<Config>       → "我能从配置中提取出来"

第五步:为具体结构体实现泛型 trait

struct UserId(u32);

// UserId 能从 HttpRequest 中提取
impl Extract<HttpRequest> for UserId {
    fn extract(source: &HttpRequest) -> Self {
        let id: u32 = source.path_param("id").parse().unwrap();
        UserId(id)
    }
}

// UserId 也能从 Config 中提取
impl Extract<Config> for UserId {
    fn extract(source: &Config) -> Self {
        UserId(source.default_user_id)
    }
}

同一个 UserId,对不同的 S 有不同的提取逻辑。这就是泛型 trait 的意义——同一个行为,针对不同的上下文有不同的实现

第六步:泛型结构体 + 泛型 trait

struct Wrapper<T> {
    value: T,
}

trait Extract<S> {
    fn extract(source: &S) -> Self;
}

impl<T: FromStr> Extract<HttpRequest> for Wrapper<T> {
    fn extract(source: &HttpRequest) -> Self {
        let raw: &str = source.body_str();
        let value: T = raw.parse().unwrap();
        Wrapper { value }
    }
}

读法:“对于任何能从字符串解析的类型 T,Wrapper<T> 都能从 HttpRequest 中提取”

Wrapper<u32>    能从 HttpRequest 提取 → 解析 body 为 u32
Wrapper<f64>    能从 HttpRequest 提取 → 解析 body 为 f64
Wrapper<String> 能从 HttpRequest 提取 → 解析 body 为 String

这就是 axum 的 Json<T> 的原理。

第七步:回看 axum

// axum 的 trait(简化)
trait FromRequest<S> {
    fn from_request(req: Request, state: &S) -> Result<Self, Error>;
}

// axum 的 Json(简化)
struct Json<T>(pub T);

impl<S, T: DeserializeOwned> FromRequest<S> for Json<T> {
    fn from_request(req: Request, _state: &S) -> Result<Self, Error> {
        let bytes = read_body(req);
        let value: T = serde_json::from_slice(&bytes)?;
        Ok(Json(value))
    }
}

把泛型”实例化”来看:

Json<Player>  的 from_request → 读 body → 反序列化为 Player → 返回 Json(player)
Json<Item>    的 from_request → 读 body → 反序列化为 Item   → 返回 Json(item)
Json<String>  的 from_request → 读 body → 反序列化为 String → 返回 Json(string)

你写 Json<Player> 时,编译器把 T 替换成 Player,生成一份专用代码。

第八步:泛型 trait 参数不约束 = 对所有类型成立

axum 的 FromRequest<S> 中 S 是应用状态的类型。不同 extractor 对 S 的态度不同:

// Json:不关心 S 是什么(解析 body 跟应用状态无关)
impl<S, T: DeserializeOwned> FromRequest<S> for Json<T> { ... }
//   ↑
//   不约束 S → "不管你 state 是什么类型,我都能从 body 提取"

// Path:也不关心 S(从 URL 提取跟应用状态无关)
impl<S, T: DeserializeOwned> FromRequestParts<S> for Path<T> { ... }
//   ↑
//   不约束 S → "不管你 state 是什么类型,我都能从 URL 提取"

// State:必须知道 S 是什么(因为它要返回 S 本身)
impl<S: Clone> FromRequestParts<S> for State<S> { ... }
//   ↑                                      ↑
//   约束 S: Clone                    State 提取出来的就是 S 本身

对比表:

Extractor   对 S 的态度       原因
──────────────────────────────────────────────────
Json<T>     impl<S>          解析 body 不需要 state
Path<T>     impl<S>          解析 URL 不需要 state
Query<T>    impl<S>          解析查询参数不需要 state
HeaderMap   impl<S>          读请求头不需要 state
State<S>    impl<S: Clone>   它返回的就是 state 本身,必须能 clone

不约束 = 对所有 S 都成立 = 不管你 with_state() 传了什么类型都能用。


分享这篇文章:

上一篇
系统设计基础(三):前台、中台、后台的兴衰与演进
下一篇
系统设计基础(二):System Design 到底关注什么