跳转到正文
zeno's blog
返回

Rust 泛型(一):总览

专题: Rust 基础

Table of contents

Open Table of contents

泛型解决什么问题

没有泛型时,同样的逻辑要为每个类型写一遍:

fn max_i32(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}

fn max_f64(a: f64, b: f64) -> f64 {
    if a > b { a } else { b }
}

fn max_string(a: String, b: String) -> String {
    if a > b { a } else { b }
}

// 逻辑完全一样,只是类型不同

泛型:把类型变成参数,写一次逻辑,适用于所有类型。

fn max<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

max(1, 2);           // T = i32
max(3.14, 2.71);     // T = f64
max("abc", "xyz");   // T = &str

1. 泛型函数

// T 是类型参数,调用时由编译器推断
fn first<T>(list: &[T]) -> &T {
    &list[0]
}

let nums = vec![1, 2, 3];
let s = first(&nums);       // T = i32

let names = vec!["Alice", "Bob"];
let s = first(&names);      // T = &str

多个类型参数

fn zip_two<A, B>(a: A, b: B) -> (A, B) {
    (a, b)
}

let pair = zip_two(42, "hello");  // A = i32, B = &str

trait bound:约束 T 能做什么

光写 T 没有任何约束——编译器不知道 T 有什么方法可以调用。

fn print_twice<T>(val: T) {
    println!("{}", val);  // 编译错误!T 不一定实现了 Display
}

// 加上 trait bound:T 必须实现 Display
fn print_twice<T: std::fmt::Display>(val: T) {
    println!("{}", val);  // OK
    println!("{}", val);
}

// 多个约束用 +
fn process<T: Display + Debug + Clone>(val: T) { ... }

// 约束太长时用 where 子句(等价写法,更清晰)
fn process<T>(val: T)
where
    T: Display + Debug + Clone,
{
    ...
}

2. 泛型结构体

// 一个可以装任何类型的容器
struct Container<T> {
    value: T,
}

let c1 = Container { value: 42 };         // Container<i32>
let c2 = Container { value: "hello" };    // Container<&str>
let c3 = Container { value: vec![1, 2] }; // Container<Vec<i32>>

多个类型参数

struct Pair<A, B> {
    first: A,
    second: B,
}

let p = Pair { first: 1, second: "one" };  // Pair<i32, &str>

标准库中的泛型结构体

你一直在用的这些全是泛型结构体:

Vec<T>              // 动态数组,T 是元素类型
HashMap<K, V>       // 哈希表,K 是键类型,V 是值类型
Option<T>           // 可能有值也可能没有
Result<T, E>        // 成功时是 T,失败时是 E
Box<T>              // 堆上的值
Arc<T>              // 引用计数的共享指针

实际例子:游戏服务端

// API 的统一响应结构
struct ApiResponse<T> {
    success: bool,
    data: Option<T>,
    error: Option<String>,
}

// T = Player
let resp: ApiResponse<Player> = ApiResponse {
    success: true,
    data: Some(player),
    error: None,
};

// T = Vec<Item>
let resp: ApiResponse<Vec<Item>> = ApiResponse {
    success: true,
    data: Some(items),
    error: None,
};

3. 泛型方法(impl 块上的泛型)

为泛型结构体实现方法

struct Container<T> {
    value: T,
}

// 为所有 Container<T> 实现的方法(无约束)
impl<T> Container<T> {
    fn new(value: T) -> Self {
        Container { value }
    }

    fn into_inner(self) -> T {
        self.value
    }

    fn as_ref(&self) -> &T {
        &self.value
    }
}

let c = Container::new(42);
let val = c.into_inner();  // val = 42

只对特定 T 实现的方法

// 只有 T: Display 时才有 show 方法
impl<T: std::fmt::Display> Container<T> {
    fn show(&self) {
        println!("Container holds: {}", self.value);
    }
}

Container::new(42).show();       // OK,i32 实现了 Display
Container::new(vec![1]).show();  // 编译错误!Vec<i32> 没实现 Display

为特定具体类型实现的方法

// 只有 Container<f64> 才有 sqrt 方法
impl Container<f64> {
    fn sqrt(&self) -> f64 {
        self.value.sqrt()
    }
}

Container::new(9.0).sqrt();   // OK
Container::new(9_i32).sqrt(); // 编译错误!i32 版本没有 sqrt 方法

方法自身也可以有泛型参数

impl<T> Container<T> {
    // 方法的泛型参数 U,独立于结构体的 T
    fn map<U, F>(self, f: F) -> Container<U>
    where
        F: FnOnce(T) -> U,
    {
        Container { value: f(self.value) }
    }
}

let c = Container::new(42);
let c2 = c.map(|n| n.to_string());  // Container<i32> → Container<String>

4. 泛型 Trait

定义泛型 trait

// 可以把自己转换成某种类型 T
trait Into<T> {
    fn into(self) -> T;
}

// 可以从某种类型 T 创建自己
trait From<T> {
    fn from(value: T) -> Self;
}

这就是标准库中 From/Into 的定义。T 是”转换的目标/来源类型”。

实现泛型 trait

struct PlayerId(u32);

// PlayerId 可以从 u32 创建
impl From<u32> for PlayerId {
    fn from(id: u32) -> Self {
        PlayerId(id)
    }
}

// PlayerId 可以从字符串解析
impl From<&str> for PlayerId {
    fn from(s: &str) -> Self {
        PlayerId(s.parse().unwrap())
    }
}

let id1 = PlayerId::from(42_u32);   // From<u32>
let id2 = PlayerId::from("123");    // From<&str>

关联类型 vs 泛型参数

trait 上的泛型有两种写法,含义不同:

// 泛型参数:一个类型可以实现多次(每个 T 一次)
trait From<T> {
    fn from(val: T) -> Self;
}
// PlayerId 可以同时 impl From<u32> 和 impl From<&str>
// 因为 T 不同,是两个不同的 trait 实现

// 关联类型:一个类型只能实现一次(类型唯一确定)
trait Iterator {
    type Item;  // 关联类型
    fn next(&mut self) -> Option<Self::Item>;
}
// Vec<i32> 的 Iterator::Item 只能是 i32,不可能同时是 String
// 因为"迭代出什么类型"对一个迭代器来说是确定的

选择规则


5. Trait Bound 的各种写法

// 写法 1:尖括号里
fn foo<T: Clone + Debug>(val: T) { ... }

// 写法 2:where 子句(推荐,复杂时更清晰)
fn foo<T>(val: T)
where
    T: Clone + Debug,
{ ... }

// 写法 3:impl Trait(参数位置,语法糖)
fn foo(val: impl Clone + Debug) { ... }

// 三种写法等价(几乎,impl Trait 有细微差别)

返回值中的 impl Trait

// "返回某个实现了 Iterator 的类型,具体是什么调用者不需要知道"
fn make_counter() -> impl Iterator<Item = u32> {
    0..100
}

// 不用 impl Trait 的话,你要写出具体类型名(可能巨长)
fn make_counter() -> std::ops::Range<u32> {
    0..100
}

常见的 trait bound 组合

// 可以打印调试信息
T: Debug

// 可以克隆
T: Clone

// 可以比较大小
T: PartialOrd

// 可以序列化/反序列化(serde)
T: Serialize + DeserializeOwned

// 可以跨线程发送 + 共享
T: Send + Sync

// 可以作为 HashMap 的 key
T: Hash + Eq

// 可以在异步 task 中使用
T: Send + 'static

6. 泛型的编译:单态化(Monomorphization)

Rust 的泛型是零成本抽象——编译时展开,运行时没有额外开销。

fn max<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

max(1_i32, 2_i32);    // 使用了 T = i32
max(3.14_f64, 2.71);  // 使用了 T = f64

编译器看到两种具体类型被使用,生成两份具体代码:

// 编译器实际生成的(概念上)
fn max_i32(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}

fn max_f64(a: f64, b: f64) -> f64 {
    if a > b { a } else { b }
}

好处:运行时直接调用具体函数,没有虚函数表查找,没有运行时类型判断。性能和手写每个类型一样。

代价:每种类型组合生成一份代码,编译产物可能更大、编译更慢。

对比:动态分发(dyn Trait)

// 静态分发(泛型):编译时确定类型,生成具体代码
fn process<T: Display>(val: T) { println!("{}", val); }
// 编译器知道 T 是什么,内联优化,零开销

// 动态分发(trait object):运行时通过虚表查找方法
fn process(val: &dyn Display) { println!("{}", val); }
// 编译器不知道具体类型,运行时查表调方法,有微小开销
// 但只生成一份代码,编译产物更小

什么时候用 dyn


7. 实际例子:tower 的 Service 和 Layer

现在回头看 tower 的抽象,全是泛型:

// Service 对 Request 泛型
trait Service<Request> {
    type Response;
    type Error;
    fn call(&mut self, req: Request) -> ...;
}

// Layer 对 S(内层 Service)泛型
trait Layer<S> {
    type Service;
    fn layer(&self, inner: S) -> Self::Service;
}

// TimeoutLayer 可以包装 **任何** Service
// 因为它对 S 完全泛型
impl<S> Layer<S> for TimeoutLayer {
    type Service = TimeoutService<S>;
    fn layer(&self, inner: S) -> TimeoutService<S> {
        TimeoutService { inner, duration: self.duration }
    }
}

// TimeoutService 对 S 和 Request 都泛型
impl<S, Request> Service<Request> for TimeoutService<S>
where
    S: Service<Request>,  // 内层必须也是 Service
{
    type Response = S::Response;  // 透传内层的响应类型
    type Error = BoxError;

    fn call(&mut self, req: Request) -> ... {
        tokio::time::timeout(self.duration, self.inner.call(req))
    }
}

这就是为什么同一个 TimeoutLayer 能同时用在 HTTP handler 和 gRPC handler 上——它对请求和响应类型完全泛型,只要内层是 Service 就行。


8. 总结

泛型函数:   fn foo<T>(val: T)              把类型当参数传
泛型结构体: struct Foo<T> { val: T }       数据结构与类型无关
泛型方法:   impl<T> Foo<T> { fn bar() }   方法与类型无关
泛型 trait: trait Foo<T> { fn bar(T) }     行为与类型无关
trait bound: T: Clone + Debug              约束 T 必须有哪些能力

编译时: 单态化 → 为每种具体类型生成专用代码 → 零运行时开销
运行时: 和手写每种类型一模一样,没有额外成本

分享这篇文章:

上一篇
系统设计基础(二):System Design 到底关注什么
下一篇
系统设计基础(一):C4 Model 如何用四层缩放解决架构图混乱