前置知识: Rust

泛型与 Trait

5 min中级

泛型函数与结构体、Trait 定义实现、Trait 对象与生命周期标注

1. 从”万能插座”说起:泛型

想象一个万能插座:无论插头是两脚还是三脚、圆形还是方形,都能插进去用。泛型(Generic)就是代码世界的”万能插座”——让函数与类型对”任意类型”工作,编译时再针对每个具体类型生成专用代码(单态化,monomorphization),运行时零开销。

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut max = &list[0];
    for item in list {
        if item > max {
            max = item;
        }
    }
    max
}

fn main() {
    println!("{}", largest(&[1, 5, 3]));          // 5
    println!("{}", largest(&['a', 'z', 'm']));    // z
}

讲解:<T> 声明泛型参数;T: PartialOrd 是 trait 约束——T 必须支持比较运算。同一个函数既能处理整数数组又能处理字符数组,类型在调用处推断。

1.1 泛型结构体与方法

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }
}

// 针对特定类型的专门实现:只有 Point<f64> 有该方法
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

fn main() {
    let p = Point::new(1, 2);
    let pf = Point { x: 3.0, y: 4.0 };
    println!("{}", pf.distance_from_origin()); // 5.0
}

讲解:impl<T> Point<T> 为所有类型实现方法;impl Point<f64> 只为 f64 类型实现专属方法。泛型与具体类型可实现不同行为。

2. Trait:行为的抽象接口

Trait 定义一组方法签名,相当于其他语言里的”接口”(interface)。它实现的是行为复用:结构体/枚举可以共享同一种行为定义。

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct Article {
    pub title: String,
    pub author: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("《{}》 by {}", self.title, self.author)
    }
}

pub struct Tweet {
    pub user: String,
    pub content: String,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{} 说:{}", self.user, self.content)
    }
}

讲解:trait 只声明签名,impl ... for ... 为具体类型实现。Article 与 Tweet 形态不同,但都具备 summarize 行为——调用方无需关心具体类型。

2.1 默认实现

pub trait Summary {
    fn summarize_author(&self) -> String;         // 必须实现
    fn summarize(&self) -> String {               // 默认实现,可覆盖
        format!("作者:{}", self.summarize_author())
    }
}

讲解:带默认实现的 trait 方法,实现者可以只实现必须的方法;默认实现内部可以调用其他 trait 方法(本例调用必须方法)。

2.2 作为参数与返回类型

fn print_summary(item: &impl Summary) {       // impl Trait 语法(语法糖)
    println!("{}", item.summarize());
}

fn make_summary(kind: bool) -> impl Summary {  // 返回实现了 Summary 的类型
    if kind {
        Tweet { user: String::from("a"), content: String::from("hi") }
    } else {
        Article { title: String::from("t"), author: String::from("b") }
    }
}

讲解:&impl Summary 是”任何实现 Summary 的类型”;返回 impl Summary 要求单一具体类型(上面 if/else 返回不同类型会报错)。需要运行时多态时用 Trait 对象:

2.3 Trait 对象(动态分发)

fn make_summary_dyn(kind: bool) -> Box<dyn Summary> {  // 动态分发
    if kind {
        Box::new(Tweet { user: String::from("a"), content: String::from("hi") })
    } else {
        Box::new(Article { title: String::from("t"), author: String::from("b") })
    }
}

讲解:dyn Summary 是 trait 对象,借助 Box 装箱后可以在运行时选择不同实现(动态分发),代价是一次虚表间接调用。对比:泛型/impl Trait 是编译期静态分发,性能更优但类型必须确定。

3. 常用标准库 Trait

Trait用途示例
Debug{:?} 调试输出#[derive(Debug)]
Display{} 用户输出实现 fmt 方法
Clone深拷贝x.clone()
Copy按位复制(栈类型)#[derive(Copy, Clone)]
PartialEq / Eq相等比较x == y
PartialOrd / Ord大小比较、排序v.sort()
Default默认值#[derive(Default)]
Iterator迭代器协议见集合与迭代器一篇
From / Into类型转换String::from(s)

通过 #[derive(...)] 派生这些 trait 是 Rust 最常用的”免费实现”:

#[derive(Debug, Clone, PartialEq, Default)]
struct Config {
    host: String,
    port: u16,
}

fn main() {
    let c1 = Config::default();                       // Default
    let c2 = c1.clone();                              // Clone
    println!("{:?}", c1);                             // Debug
    println!("{}", c1 == c2);                         // PartialEq
}

讲解:derive 只能用于”各字段也实现了该 trait”的类型;这大幅减少样板代码。自定义 Display 需要手写 fmt 方法(见错误处理一篇的自定义错误示例)。

4. 生命周期标注

生命周期(Lifetime)描述引用有效的范围,主要出现在”返回的引用来自参数”这类场景。

4.1 生命周期省略规则

多数情况编译器自动推断,无需标注:

fn first(s: &str) -> &str {   // 单输入单输出:自动省略
    &s[..1]
}

需要标注的典型场景——多个输入引用:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("long");
    let s2 = String::from("s");
    let r = longest(&s1, &s2);   // 返回的引用与 s1、s2 中较短者生命周期一致
    println!("{r}");
}

讲解:<'a> 是生命周期参数声明;约束”x、y 和返回值共用生命周期 ‘a”。编译器据此保证:返回的引用不会悬垂——即调用方不能让它活得比 s1/s2 更长。

4.2 生命周期与结构体

结构体持有引用时也必须标注:

struct Excerpt<'a> {
    part: &'a str,   // 该引用至少与结构体活得一样久
}

fn main() {
    let novel = String::from("hello world");
    let e = Excerpt { part: &novel[..5] };
    println!("{}", e.part);
}

讲解:Excerpt<'a> 表示结构体的生命周期参数;持有引用的结构体必须显式标注,编译器据此检查”引用不会比结构体先失效”。

4.3 ‘static 生命周期

let s: &'static str = "字符串字面量";   // 字面量存活于整个程序

讲解:'static 表示引用存活到程序结束,只用于字面量、常量等场景;不要因为”生命周期报错”就盲目加 'static——大多数报错应通过调整结构解决。

5. 泛型 + Trait + 生命周期组合

一个完整的”可比较、可打印”泛型函数:

use std::fmt::Display;

fn announce<T: Display + Clone>(label: &str, value: &T) {
    let cloned = value.clone();
    println!("{label}: {cloned}");
}

fn main() {
    announce("数字", &42);
    announce("文本", &"hello".to_string());
}

讲解:T: Display + Clone 表示 T 必须同时实现两个 trait(多约束);参数 &T 是借用,内部需要所有权时用 clone。泛型函数最常见的三个关键词组合:Display(打印)、Clone(复制)、借用引用(避免移动)。

6. 进阶:trait 中的 async fn 与返回 impl Trait

Rust 1.75(2023-12 起)正式支持在 trait 中直接写 async fn 与返回 impl Trait(RPITIT),异步接口不再必须依赖 async-trait 宏:

trait Fetcher {
    // trait 方法原生支持 async:等价于返回 impl Future<Output = Data>
    async fn fetch(&self, url: &str) -> String;

    // 也支持返回位置 impl Trait
    fn items(&self) -> impl Iterator<Item = u32>;
}

struct HttpFetcher;

impl Fetcher for HttpFetcher {
    async fn fetch(&self, url: &str) -> String {
        format!("来自 {url} 的数据")
    }
    fn items(&self) -> impl Iterator<Item = u32> {
        1..=3
    }
}

讲解:这一特性让 trait 定义异步接口的样板大幅减少。两个当前仍需了解的限制:其一,含 async fn 的 trait 暂时不能做 trait 对象(dyn Fetcher 编译不过)——需要动态分发时仍用 async-trait crate(它把 async 方法改写成 Box<dyn Future>)或枚举分发;其二,返回的 Future 的具体类型由实现方决定,跨线程 spawn 时可能需要 Send 约束辅助。Rust 2024 edition 进一步改进了 impl Trait 的生命周期捕获规则(默认捕获所有输入生命周期),写泛型异步代码时语义更符合直觉。

7. 小结

泛型解决”对多种类型写一份代码”,Trait 定义”行为的接口”,生命周期保证”引用不悬垂”。三者组合让 Rust 既能写出抽象代码,又保持零运行时开销与内存安全。下一步进入测试与调试,学会验证自己的代码。

一句话记忆:Rust 抽象三件套——“泛型(<T>)对多种类型写一份代码、Trait(impl ... for)定义行为接口、生命周期('a)保证引用不悬垂”;需要多态时,编译期选泛型(静态分发)、运行期选 Box<dyn Trait>(动态分发)。