Skip to content

泛型与 trait bound

本章是类型系统的深化章:把「会写泛型函数」升级为「能读懂编译器关于泛型与生命周期的每一条报错」。 前置知识:所有权与借用结构体、枚举与 trait。本章假定你已经会写 fn largest<T: PartialOrd>(list: &[T]) -> &T 这样的基本泛型函数。

本章目标

  • 能说清泛型(generics)在编译期如何被单态化(monomorphization)成具体代码,以及这个选择的性能与编译代价。
  • 能熟练使用 trait bound(T: A + Bwhere 子句、impl Trait 参数/返回位置),并知道三者的取舍。
  • 能解释关联类型(associated type)与泛型参数的区别,知道 Iterator::ItemAdd<Rhs> 为什么设计得不一样。
  • 能读懂并写出生命周期(lifetime)标注,理解 'a 是约束而非时长、三条省略规则、变型(variance)表。
  • 能识别 8 类最常见的泛型/生命周期编译错误,并知道每一类的标准修法。
  • 能解释 dyn Traitimpl Trait、trait 默认方法、孤儿规则(orphan rule)背后的机制。

泛型基础与单态化

四种泛型位置

泛型(generics)在 Rust 里可以出现在四个位置:函数、结构体、枚举、方法(含 impl 块)。 下面这个例子把四种位置都演示一遍,并展示具体类型上的方法impl Point<f64>)与泛型方法自己的泛型参数

rust
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    // 从第一个元素开始比较;调用方必须保证 list 非空,否则这里会 panic
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

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

// 对所有 T 都成立的方法
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

// 只对 f64 成立的方法:T 已经被写死成具体类型
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

struct Mixed<T, U> {
    first: T,
    second: U,
}

impl<T, U> Mixed<T, U> {
    // 方法可以引入自己的泛型参数 V、W,与 impl 块上的 T、U 相互独立
    fn swap_second<V, W>(self, other: Mixed<V, W>) -> Mixed<T, W> {
        Mixed { first: self.first, second: other.second }
    }
}

enum MyOption<T> {
    Nothing,
    Something(T),
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    println!("largest = {}", largest(&numbers));

    let chars = vec!['y', 'm', 'a', 'q'];
    println!("largest char = {}", largest(&chars));

    let p = Point { x: 3.0, y: 4.0 };
    println!("x = {}, dist = {}", p.x(), p.distance_from_origin());

    let m = Mixed { first: 1, second: "one" };
    let m2 = m.swap_second(Mixed { first: 'a', second: 2.5 });
    println!("{} {}", m2.first, m2.second);

    let _o: MyOption<i32> = MyOption::Something(1);
}

输出:

largest = 100
largest char = y
x = 3, dist = 5
1 2.5

两条容易忽略的规则:

  1. impl<T> Point<T>impl Point<f64> 可以共存;调用 p.distance_from_origin() 时,编译器先按接收者类型 筛选候选 impl 块,再解析方法名。写 Point { x: 1, y: 2 }.distance_from_origin() 会得到「方法不存在」的报错, 因为 i32 不在 impl Point<f64> 覆盖范围内。
  2. impl Point<f64> 里的 SelfPoint<f64>,不是 Point<T>——在只对具体类型生效的 impl 块里不能再写 T

💡 对照:Java 的 <T extends Comparable<T>> 写在方法/类上;C++ 用 template <typename T>; Go 用 func Largest[T cmp.Ordered](list []T) *T。Rust 的形式最接近 C++ 模板, 但语义上更接近「带接口约束的泛型」——约束在定义处就必须写清楚。

单态化:泛型是零成本的

Rust 泛型不是运行期机制。编译器在看到每个实际类型实参时,把泛型代码复制一份并替换类型参数, 生成独立的机器码。这个过程叫单态化(monomorphization)。

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

fn main() {
    // 编译器实际生成 largest_i32 与 largest_char 两个函数
    println!("{}", largest(&[3, 9, 4]));
    println!("{}", largest(&['a', 'z']));
}

🧠 原理:单态化之后,largest::<i32> 内部调用的是 i32::gt,没有虚表、没有指针间接、 没有装箱。这就是 Rust 文档里「泛型是零成本抽象(zero-cost abstraction)」的含义: 不为你不用的东西付费,且你用的东西手写也快不了

代价有两项:

  • 代码膨胀(code bloat):每个实例化类型一份机器码。10 个类型实参 × 20 个泛型函数 = 200 份函数体。 二进制变大,指令缓存(i-cache)命中率下降。
  • 编译时间:单态化发生在编译期,泛型嵌套越深、类型实参越多,编译越慢。 库作者常用「外层泛型 + 内层非泛型核心」把单态化收敛到一处(例如把 &str 核心逻辑抽成非泛型函数, 泛型包装只做一次转换)。

💡 对照:Java 泛型是类型擦除(type erasure)——List<String>List<Integer> 在字节码里 都是 List,类型实参只存在于编译期用于插入强制转换。因此:

  • Java 的泛型容器必须装箱List<Integer> 实际存 Integer 对象),有堆分配与指针追逐; Rust 的 Vec<i32> 是一段连续的 i32
  • Java 不能 new T[]、不能对 Tinstanceof、不能重载仅泛型参数不同的方法; Rust 在单态化后这些都是普通具体类型,完全可用。
  • Java 可以用反射看到擦除后的原始类型;Rust 没有运行期泛型信息,TypeId 只能拿到具体类型。
  • 反向代价:Java 一份字节码服务所有类型(代码小、编译快),Rust 换取运行速度与表达力。

C++ 模板与 Rust 单态化机制最像,区别是 C++ 用 SFINAE / concepts 约束,错误信息更长; Go 1.18+ 泛型默认用 GC shape 字典(近似共享一份代码),性能模型又不同。

泛型不是「万能容器」

largest 的定义里有个隐含约定:list 不能为空,否则 &list[0] 会 panic。泛型函数里没有「类型即契约」的 保护,业务约束仍要靠返回值表达。更稳妥的写法是返回 Option<&T>

rust
fn largest_or_none<T: PartialOrd>(list: &[T]) -> Option<&T> {
    // 用迭代器的 reduce 规避索引越界:空列表自然得到 None
    list.iter().reduce(|acc, item| if item > acc { item } else { acc })
}

fn main() {
    println!("{:?}", largest_or_none(&[3, 9, 4]));
    println!("{:?}", largest_or_none::<i32>(&[]));
}

输出:

Some(9)
None

⚠️ 陷阱largest_or_none::<i32>(&[]) 里的 turbofish 是必需的——空切片推不出 T。 这类「类型推断失败」错误(type annotations needed)在泛型代码里非常常见, 修法就是显式给出类型实参或标注 let v: Vec<i32> = vec![];


trait bound 全解

trait bound(trait 约束)回答一个问题:这个泛型参数能做什么。Rust 没有「鸭子类型」, 你能调用的每个方法、每个运算符,都必须在约束里出现。

内联约束与 where 子句

rust
use std::fmt::Debug;
use std::fmt::Display;

// 内联写法:约束少、短,读起来紧凑
fn show_pair<T: Display + Clone, U: Debug>(a: T, b: U) -> String {
    let copy = a.clone();
    format!("{copy} / {b:?}")
}

// where 写法:约束多、涉及关联类型或非参数类型时更清晰
fn dump<T>(item: &T) -> String
where
    T: Named,
    T::Id: Debug,
{
    format!("{:?}:{}", item.id(), item.name())
}

trait Named {
    type Id;
    fn id(&self) -> Self::Id;
    fn name(&self) -> &str;
}

#[derive(Clone)]
struct User {
    id: u64,
}

impl Named for User {
    type Id = u64;
    fn id(&self) -> Self::Id {
        self.id
    }
    fn name(&self) -> &str {
        "user"
    }
}

fn main() {
    println!("{}", show_pair(1, "two"));
    println!("{}", dump(&User { id: 7 }));
}

输出:

1 / "two"
7:user

什么时候必须用 where

场景例子能否内联
约束对象不是泛型参数本身,而是它的关联类型where T::Id: Debug不能(老版语法无法在 <> 里写关联类型约束)
约束对象是复合类型where Vec<T>: Clone不能
高阶生命周期约束(HRTB)where F: for<'a> FnMut(&'a str) -> bool不能(for<'a> 只能出现在 where 里)
约束很长、多个参数共享where T: Display, U: Clone, V: Debug能,但横向超宽、难读
函数签名想强调参数顺序库里常见风格能,但一致性更重要

⚠️ 陷阱where Vec<T>: Clone 看起来像「随便写个约束让编译器别抱怨」,实际上它等价于要求 T: Clone,而且编译器只会告诉你「Vec<T>: Clone 不成立」,再让你自己去推。写成 T: Clone 报错更友好:

rust
// ❌ 无法编译:where Vec&lt;T&gt;: Clone 并不会自动把 T: Clone 加进来
fn report&lt;T&gt;(items: &amp;[T]) -&gt; String
where
    T: std::fmt::Display,
    Vec&lt;T&gt;: Clone,
{
    format!("{:?} -> {}", items.to_vec(), items.len())
}

fn main() {
    println!("{}", report(&[1, 2, 3]));
}
error[E0277]: the trait bound `T: Clone` is not satisfied
 --> src/main.rs:6:33
  |
6 |     format!("{:?} -> {}", items.to_vec(), items.len())
  |                                 ^^^^^^ the trait `Clone` is not implemented for `T`
  |
note: required by a bound in `slice::<impl [T]>::to_vec`
help: consider further restricting type parameter `T` with trait `Clone`
  |
2 |     T: std::fmt::Display + std::clone::Clone,
  |                          +++++++++++++++++++

修法:把约束写成 T: std::fmt::Display + Clone,并把 Vec<T>: Clone 删掉。

在泛型参数上调用方法

规则很简单:方法所在 trait(或固有 impl)必须出现在约束里。约束写得越少,函数越通用, 但内部能做的事也越少——这是一个需要刻意权衡的取舍。

rust
use std::fmt::Display;

// 只约束 Display:内部只能用 to_string / 格式化
fn tag<T: Display>(value: T) -> String {
    format!("<{value}>")
}

// 约束 Clone:内部可以复制
fn duplicate<T: Clone + Display>(value: T) -> (T, String) {
    (value.clone(), tag(value))
}

fn main() {
    let (n, text) = duplicate(3.5);
    println!("{n} {text}");
}

输出:3.5 <3.5>

💡 对照:Java 的 <T extends Comparable<T>><T extends CharSequence & Serializable> 语法上就是 多重上界;Rust 的 T: A + B 一一对应。区别是 Rust 的约束在定义处就要写全, 并且错误发生在调用处T 不满足约束)而不是方法体内。

impl Trait 参数位置(APIT)与泛型参数的等价性

下面两个函数对调用者来说几乎完全等价

rust
use std::fmt::Display;

fn show_all<T: Display>(items: Vec<T>) {
    for item in items {
        println!("{item}");
    }
}

// impl Trait 出现在参数位置(APIT)=一个匿名的泛型参数
fn show_all_impl(items: Vec<impl Display>) {
    for item in items {
        println!("{item}");
    }
}

fn main() {
    show_all(vec![1, 2]);
    show_all_impl(vec!['a', 'b']);
}

两者都会为每个实际类型单态化一份代码。差异只有两条,但都很重要:

  1. APIT 不能被 turbofish 指定show_all::<i32>(...) 合法,show_all_impl::<i32>(...) 报错:
error[E0107]: function takes 0 generic arguments but 1 generic argument was supplied
 --> src/main.rs:2:21
  |
2 | fn total(values: impl Iterator<Item = i32>) -> i32 { values.sum() }
  |                     ^^^^^--------------------------- help: remove the unnecessary generics
  |
  = note: `impl Trait` cannot be explicitly specified as a generic argument
  1. APIT 无法表达「两个参数必须是同一类型」fn pair(a: impl Display, b: impl Display)ab两个不同的匿名类型参数;要让它们相同,必须用具名泛型:
rust
use std::fmt::Display;

// 两个 impl Trait 是各自独立的匿名参数:可以传不同
fn loose(a: impl Display, b: impl Display) -> String {
    format!("{a}{b}")
}

// 具名参数可以强制相同
fn strict<T: Display>(a: T, b: T) -> String {
    format!("{a}{b}")
}

fn main() {
    println!("{}", loose(1, "x"));
    println!("{}", strict(1, 2));
}

结论:库的公开 API 优先用具名泛型(可以 turbofish、语义精确); 局部辅助函数用 APIT(签名短),因为调用者通常不需要指定类型。

impl Trait 返回位置(RPIT)

返回位置(return-position impl Trait, RPIT)表示「我返回某个实现了该 trait 的类型,但不想说出它是谁」:

rust
// 返回的迭代器类型名又长又不可写,用 impl Iterator 隐藏它
fn evens(input: &[i32]) -> impl Iterator<Item = i32> + '_ {
    input.iter().copied().filter(|n| n % 2 == 0)
}

fn main() {
    let data = [1, 2, 3, 4, 5, 6];
    let collected: Vec<i32> = evens(&data).collect();
    println!("{collected:?}");
}

输出:[2, 4, 6]

RPIT 的三个关键性质:

  • 只有一个隐藏类型。同一个函数的所有返回路径必须返回同一个具体类型(见练习 6)。
  • 调用者看不见具体类型,所以不能调用隐藏类型特有的方法,只能调用 impl 后面列出的 trait。
  • + '_ 是捕获(capture)声明:它告诉编译器「这个不透明类型可能借用输入的生命周期」。 上面 evens 的返回值借用了 input,所以必须写 + '_(或 + 'a,如果输入是具名生命周期)。

2024 edition 的捕获规则变化(重点):RFC 3498 规定,从 Rust 2024 起,RPIT 在没有 use<..> 约束时 隐式捕获所有在作用域内的生命周期参数(此前只捕获在约束里语法出现过的那些)。所以:

rust
// Rust 2021:报错 E0700(隐藏类型 &str 捕获了 '_,但签名里没提)
// Rust 2024:默认捕获作用域内所有生命周期,直接通过
fn first_word(text: &str) -> impl AsRef<str> {
    text
}

fn main() {
    println!("{}", first_word("hello world").as_ref());
}

输出:hello world

对应 2021 edition 的报错原文与迁移方式
error[E0700]: hidden type for `impl AsRef<str>` captures lifetime that does not appear in bounds
 --> src/main.rs:1:5
  |
1 | fn first_word(text: &str) -> impl AsRef<str> {
  |                     ----     --------------- opaque type defined here
  |                     |
  |                     hidden type `&str` captures the anonymous lifetime defined here
2 |     text
  |     ^^^^
help: add a `use<...>` bound to explicitly capture `'_`
  |
1 | fn first_word(text: &str) -> impl AsRef<str> + use<'_> {
  |                                              +++++++++

迁移手段(三种都仍然合法):

rust
// 1) 2024 的精确捕获语法(1.82+,所有 edition 可用)
fn a(text: &str) -> impl AsRef<str> + use<'_> {
    text
}

// 2) 老式的 outlives 技巧:塞一个生命周期约束把 '_ 带进签名(旧资料里的常见做法)
fn b<'a, T: AsRef<str> + 'a>(value: T) -> impl AsRef<str> + 'a {
    value
}

// 3) 2021 里最常用的写法
fn c(text: &str) -> impl AsRef<str> + '_ {
    text
}

fn main() {
    println!("{} {} {}", a("one").as_ref(), b("two").as_ref(), c("three").as_ref());
}

输出:one two three

要点解析:2024 的变化方向是「少写标注」。反过来,如果你不希望捕获某个生命周期(例如想让返回值 满足 'static),在 2024 里必须显式写 use<>cargo fix --edition 会通过 impl_trait_overcaptures lint 提示需要加 use<..> 的位置。

🚀 进阶use<..> 是 1.82 稳定的精确捕获(precise capturing)语法。use<> 表示「什么都不捕获」, use<'a, T> 表示只捕获列出的参数。它取代了过去丑陋的 Captures<..> 技巧。 注意 APIT 产生的匿名参数无法写进 use<..>,需要改成具名泛型参数。


关联类型 vs 泛型参数

为什么 Iterator 用关联类型

rust
trait Iterator {
    type Item;                                  // 关联类型(associated type)
    fn next(&mut self) -> Option<Self::Item>;
}

Item 用关联类型而不是 trait Iterator<Item>,原因是每个类型对某个 trait 只能有一种实现: 一个 Vec<i32> 的迭代器产出 i32,也只能产出 i32。把 Item 做成泛型参数会允许 impl Iterator<String> for MyIterimpl Iterator<i32> for MyIter 并存, 于是 for x in iter 需要额外标注才知道 x 是什么类型,且更容易出现「类型推断失败」。

🧠 原理:判断标准是「一个类型能有几种实现」。

  • 只有一种 → 用关联类型(Iterator::ItemDeref::TargetAdd::Output)。
  • 可能有多种 → 用泛型参数(Add<Rhs>From<T>PartialEq<Rhs>)。

Add 就是「可能有多种」的典型:Meters 可以加 Meters,也可以加 f64,还能加 (f64, f64)

rust
use std::ops::Add;

#[derive(Debug)]
struct Meters(f64);

impl Add for Meters {
    type Output = Meters;
    fn add(self, other: Meters) -> Meters {
        Meters(self.0 + other.0)
    }
}

#[derive(Debug)]
struct Vec2(f64, f64);

impl Add<(f64, f64)> for Vec2 {
    type Output = Vec2;
    fn add(self, rhs: (f64, f64)) -> Vec2 {
        Vec2(self.0 + rhs.0, self.1 + rhs.1)
    }
}

fn main() {
    println!("{:?}", (Meters(1.0) + Meters(2.5)).0);
    println!("{:?}", (Vec2(1.0, 1.0) + (2.0, 3.0)).0);
}

输出:

3.5
4.0

标准库的定义是 trait Add<Rhs = Self>默认类型参数让最常见的「同类型相加」不用写 Add<Meters>, 同时保留了 Add<(f64, f64)> 的自由度。

关联常量

trait 里可以带常量成员,实现者各自给出值:

rust
trait Shape {
    const NAME: &'static str;
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    const NAME: &'static str = "circle";
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

fn main() {
    println!("{} {:.2}", Circle::NAME, Circle { radius: 1.0 }.area());
}

输出:circle 3.14

⚠️ 陷阱:带关联常量的 trait 不能变成 trait object(dyn Shape 会报 E0038), 因为 vtable 里没有「常量」的位置。详见「对象安全(dyn 兼容)规则清单」。

GAT:泛型关联类型

🚀 进阶(Rust 1.65+ 稳定):关联类型自己也能带泛型参数,这就是泛型关联类型 (generic associated types, GAT)。最常见的用途是借出型迭代器(lending iterator)next 返回的引用借用 self,而普通 IteratorItem 不能依赖 &mut self 的生命周期。

rust
trait LendingIterator {
    // Item 的生命周期可以跟 &mut self 的借用挂钩
    type Item<'a>
    where
        Self: 'a;

    fn next(&mut self) -> Option<Self::Item<'_>>;
}

struct Chunks<'a, T> {
    rest: &'a [T],
    width: usize,
}

impl<'a, T> LendingIterator for Chunks<'a, T> {
    type Item<'b> = &'b [T]
    where
        Self: 'b;

    fn next(&mut self) -> Option<Self::Item<'_>> {
        if self.rest.is_empty() {
            return None;
        }
        let rest = self.rest; // &'a [T] 是 Copy,先取出来避免借用冲突
        let take = self.width.min(rest.len());
        let (head, tail) = rest.split_at(take);
        self.rest = tail;
        Some(head)
    }
}

fn main() {
    let data = [1, 2, 3, 4, 5];
    let mut it = Chunks { rest: &data, width: 2 };
    while let Some(chunk) = it.next() {
        println!("{chunk:?}");
    }
}

输出:

[1, 2]
[3, 4]
[5]

⚠️ 陷阱:GAT 的 where Self: 'a 不是装饰,它是在声明「只有当 Self 活得比 'a 长, Item<'a> 才是合法类型」。漏掉它通常得到 missing required bound 之类的报错。 另外普通 for 循环依赖 Iterator(而非 GAT 版),所以借出型迭代器要手写 while let


延伸阅读


内容以 rustc 1.98.1 · Rust 2024 edition 为基准