前置知识: Rust

泛型与 Trait

7 min中级

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

知识点地图

  • 知识类别:抽象机制——泛型(编译期多态)与 Trait(行为契约),含静态/动态分发与生命周期标注。
  • 解决什么问题:「同一份逻辑服务多种类型」怎么写才不复制粘贴;接口抽象在 Rust 里的形态(不是 class 继承);报错信息里的 'a 是什么。
  • 什么时候用到:写第一个接受多种类型的函数时;设计错误类型、日志输出、迭代能力时(第 3.1 节的三个场景);读第三方库签名看到 impl Trait/dyn Trait/'a 时。

分界声明:本篇第 4 节讲生命周期标注的省略规则与最小补标(看得懂签名、写得出 'a);结构体多引用、高阶约束与复杂推断的完整体系见《生命周期深入》(rust/180-RustLifetimesDeepDive)——050 管直觉、100 管签名、180 管体系。

前置知识

  • 集合类型:迭代器与闭包的基本用法——本文会反复出现 Iterator trait。

学习目标

读完本文你将能够:

  1. 会写泛型函数与泛型结构体,理解单态化带来的零运行时开销;
  2. 会定义 trait 并为类型实现,掌握默认实现与 impl Trait 参数/返回语法;
  3. 分清静态分发(泛型)与动态分发(Box<dyn Trait>)的机制与取舍;
  4. 看懂生命周期省略规则,会在多输入引用等场景补 'a 标注;
  5. 了解 trait 中 async fn 的支持现状与 dyn 分发的限制。

预计 75 分钟。

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

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

rust
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 泛型结构体与方法

rust
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)。它实现的是行为复用:结构体/枚举可以共享同一种行为定义。

rust
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 默认实现

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

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

2.2 作为参数与返回类型

rust
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 对象(动态分发)

rust
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 最常用的”免费实现”:

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 方法(见错误处理一篇的自定义错误示例)。

3.1 三个工程场景:Trait 是「能力的契约」

表是速查,理解要靠场景。三个高频工程场景,各看「实现这个 Trait 换来了什么」:

场景一:Display——给领域类型一份可读输出(调试与日志):

rust
use std::fmt;

struct Song { title: String, bpm: u16 }

impl fmt::Display for Song {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "《{}》({} BPM)", self.title, self.bpm)
    }
}

// 日志里直接写:
// println!("加入播放队列: {song}");   -> 加入播放队列: 《ネコミミアーカイブ》(174 BPM)

Debug 与 Display 的分工:{:?} 给开发者看(derive 一行、暴露内部字段),{} 给用户/日志看(手写、控制措辞)——日志系统里用 Display 的格式做聚合键(同一首歌日志格式稳定可 grep),Debug 只进开发环境。

场景二:From——错误类型的统一收口(衔接错误处理篇):

rust
struct ParseError(String);

impl From<std::num::ParseIntError> for ParseError {
    fn from(e: std::num::ParseIntError) -> Self {
        ParseError(e.to_string())
    }
}

fn parse_bpm(raw: &str) -> Result<u16, ParseError> {
    let bpm: u16 = raw.parse()?;      // ? 用 From 把 ParseIntError 自动转成 ParseError
    Ok(bpm)
}

? 运算符背后的机制就是 From::from:每个 ? 都在问「这个错误能 From 成函数的错误类型吗」——为你的错误类型实现一批 From,全项目的 ? 链就自动打通(rust/080 的 thiserror 派生宏正是批量生成这些 impl)。

场景三:Iterator——把集合加工流水线化(衔接迭代器篇):

rust
let hot: Vec<&Song> = songs
    .iter()                       // Iterator 协议的起点
    .filter(|s| s.bpm >= 160)
    .take(10)
    .collect();                   // 任何实现 Iterator 的东西都能这样串

filter/take/collect 不是 Vec 的方法而是 Iterator trait 的默认方法——实现一次 Iterator(一个 next 方法),整条适配器流水线全部免费获得。这是「trait 即能力契约」的最直接体现:你的自定义集合只要实现 Iterator,就能被全生态的泛型函数消费(sort、sum、collect 全部就位)。零成本抽象的细节(适配器融合、惰性求值)见《迭代器与函数式风格》(rust/095-RustIteratorsFunctional)。

4. 生命周期标注

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

4.1 生命周期省略规则

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

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

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

rust
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 生命周期与结构体

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

rust
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 生命周期

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

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

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

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

rust
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 宏:

rust
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>(动态分发)。