Skip to content
Published at:

11. Traits & Generics — Trait 与泛型

编写能处理多种类型——甚至还未被定义的类型——的代码是编程中最伟大的发现之一。Vec<T> 是泛型的:你可以创建字符串向量、整数向量、任意类型的向量。FileTcpStream 都能写入数据,尽管它们是完全不同的类型。Rust 用 trait泛型实现这种多态性,其设计深受 Haskell 的 typeclass 影响。

Trait 是 Rust 对“接口”或“抽象基类”的回答——它定义了一组类型必须实现的行为。泛型则让代码对类型进行抽象,配合 trait 约束确保类型安全。

使用 Trait

一个 trait 代表一种能力Write 表示能写字节,Iterator 表示能产生序列,Clone 表示能复制自身,Debug 表示能格式化输出。

rust
use std::io::Write;

// 任何实现了 Write 的类型都可以传入
fn say_hello(out: &mut dyn Write) -> std::io::Result<()> {
    out.write_all(b"hello world\n")?;
    out.flush()
}

Rust 有一条不寻常的规则:trait 必须在作用域内才能调用其方法。这是为了防止不同 trait 的方法名冲突——当多个扩展 trait 给同一类型添加同名方法时,你必须显式导入你想用的那个:

rust
// 如果不导入 Write,就不能调用 write_all
use std::io::Write;

let mut buf: Vec<u8> = vec![];
buf.write_all(b"hello")?;  // 需要 Write 在作用域内

CloneIterator 等方法属于标准 prelude——自动导入,不需要显式 use

Trait 对象

上面的 &mut dyn Write 是一个 trait 对象——一个胖指针,包含两部分:

&mut dyn Write 占 2 个机器字:
┌──────────────────────────────┬──────────────────────────────┐
│   数据指针(指向实际值)         │   vtable 指针(指向方法表)      │
└──────────────────────────────┴──────────────────────────────┘

vtable 在编译时为每个具体类型生成一次,包含该类型对 trait 中每个方法的函数指针。当调用 out.write_all(...) 时,Rust 通过 vtable 查找正确的函数地址,这称为动态分发(dynamic dispatch)。

rust
use std::io::Write;

// 三个完全不同的类型,都实现了 Write
let mut buf: Vec<u8> = vec![];
let mut file = std::fs::File::create("log.txt")?;
let mut tcp = std::net::TcpStream::connect("127.0.0.1:8080")?;

// 都可以放入 Vec<Box<dyn Write>>
let writers: Vec<Box<dyn Write>> = vec![
    Box::new(buf),
    Box::new(file),
    Box::new(tcp),
];

for writer in &mut writers {
    say_hello(writer.as_mut())?;  // 每个调用通过各自的 vtable 分发
}

泛型函数与静态分发

泛型函数用类型参数替代 trait 对象:

rust
// 泛型版本:编译时为每个具体类型 W 生成代码
fn say_hello<W: Write>(out: &mut W) -> std::io::Result<()> {
    out.write_all(b"hello world\n")?;
    out.flush()
}

这称为单态化(monomorphization)——编译器为每个被使用的具体类型生成一份专用机器码。这带来三个关键优势:

  1. 速度:没有运行时 vtable 查找,编译器可以内联和深度优化
  2. 兼容性:某些 trait(带关联类型或 Self 返回值的)不能用作 trait 对象,但可以用于泛型
  3. 多 trait 约束:泛型可以轻松要求 T: Debug + Hash + Eq,而 trait 对象不支持多 trait 组合

代价是代码膨胀——泛型函数的每个实例化都生成一份独立的机器码。

多 trait 约束

+ 组合多个 trait bound:

rust
use std::hash::Hash;
use std::fmt::Debug;

fn top_ten<T: Debug + Hash + Eq>(values: &[T]) { /* ... */ }

where 子句

当约束变长时,where 子句让代码更清晰:

rust
fn complex_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    // ...
}

// 对比:不用 where 的版本(难读)
fn complex_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
    // ...
}

带生命周期和类型参数的泛型签名——生命周期在前:

rust
fn find<'a, T>(slice: &'a [T], predicate: impl Fn(&T) -> bool) -> Option<&'a T>
where
    T: Debug,
{
    // ...
}

泛型结构体和方法

泛型不仅限于函数:

rust
struct Pair<T> {
    first: T,
    second: T,
}

impl<T: Clone> Pair<T> {
    fn duplicate(&self) -> Pair<T> {
        Pair {
            first: self.first.clone(),
            second: self.second.clone(),
        }
    }
}

// 为特定类型添加方法(仅 Pair<i32> 有这个功能)
impl Pair<i32> {
    fn sum(&self) -> i32 {
        self.first + self.second
    }
}

Trait 对象 vs 泛型:如何选择

维度Trait 对象 (dyn Trait)泛型 (T: Trait)
分发方式动态分发(vtable)静态分发(单态化)
性能间接调用,不易内联直接调用,易内联,更快
代码大小一份代码每个类型一份,可能膨胀
异构集合支持(Vec<Box<dyn Trait>>不支持(Vec 中所有元素必须同类型)
多 trait不支持(dyn A + B 有限)完美支持(T: A + B + C
关联类型受限完全支持

经验法则:需要异构集合时用 trait 对象;追求性能或有复杂 trait 约束时用泛型。

rust
// Trait 对象——适合异构集合(不同类型的"蔬菜")
trait Vegetable { fn name(&self) -> &str; }
struct Salad {
    veggies: Vec<Box<dyn Vegetable>>,  // 可以混合不同类型
}

// 泛型——适合同构集合和性能敏感路径
fn sort_by_key<T, K: Ord>(slice: &mut [T], key_fn: impl Fn(&T) -> K) {
    // 单态化为每个 T 和 K 生成优化代码
}

定义和实现 Trait

定义 trait 的语法类似方法声明:

rust
trait Visible {
    /// 在画布上绘制自身
    fn draw(&self, canvas: &mut Canvas);

    /// 点击测试:返回自身内的点是否被命中
    fn hit_test(&self, x: i32, y: i32) -> bool;
}

为某个类型实现 trait:

rust
impl Visible for Broom {
    fn draw(&self, canvas: &mut Canvas) {
        // 使用 self.x, self.y 等字段
        for y in self.y - self.height - 1..self.y {
            canvas.write_at(self.x - 1, y, '|');
        }
    }

    fn hit_test(&self, x: i32, y: i32) -> bool {
        self.x == x
            && self.y - self.height - 1 <= y
            && y <= self.y
    }
}

默认方法

Trait 中的方法可以提供默认实现:

rust
trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;

    // 有默认实现——实现者可以选择覆盖
    fn write_all(&mut self, buf: &[u8]) -> Result<()> {
        let mut bytes_written = 0;
        while bytes_written < buf.len() {
            bytes_written += self.write(&buf[bytes_written..])?;
        }
        Ok(())
    }

    fn flush(&mut self) -> Result<()>;
}

实现者只需提供没有默认实现的方法。这类似于 Java 的接口默认方法或 C++ 的虚函数默认实现。

孤儿规则(Orphan Rule)

实现 trait 时必须遵守一条严格的规则:要么 trait,要么类型,必须定义在当前 crate 中。这被称为“孤儿规则”——你不能为外部类型实现外部 trait:

rust
// 编译错误!Write 和 Vec 都是标准库的
// impl std::io::Write for Vec<u8> { ... }

// 正确:在我们的 crate 中为外部类型实现我们的 trait
impl MyTrait for Vec<u8> { /* ... */ }

// 正确:在我们的 crate 中为我们的类型实现外部 trait
impl std::fmt::Display for MyType { /* ... */ }

孤儿规则防止两个 crate 对同一类型和 trait 的组合给出冲突的实现,保证了 trait 实现的全局一致性。

Self 类型

在 trait 定义中,Self 指代实现这个 trait 的具体类型:

rust
pub trait Clone {
    fn clone(&self) -> Self;  // 返回与调用者相同的类型
}

pub trait Spliceable {
    fn splice(&self, other: &Self) -> Self;
}

使用 Self 作为返回类型的 trait 与 trait 对象不兼容——因为编译器无法在运行时确保两个 trait 对象的实际类型相同:

rust
// 编译错误!Clone 返回 Self,不能用作 trait 对象
// fn clone_all(values: &[Box<dyn Clone>]) { ... }
// error: the trait `Clone` cannot be made into an object

关联类型 vs 泛型参数

当 trait 需要关联额外的类型时,有两种选择:

关联类型

适合“每个实现只有一个合理关联类型”的场景:

rust
pub trait Iterator {
    type Item;  // 关联类型——每个实现指定一个具体类型

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

impl Iterator for Counter {
    type Item = u32;  // Counter 产生的元素是 u32
    fn next(&mut self) -> Option<u32> { /* ... */ }
}

泛型 Trait

适合“一个类型可以以多种方式实现同一个 trait”的场景:

rust
pub trait Mul<RHS = Self> {  // RHS 是泛型参数,默认 = Self
    type Output;
    fn mul(self, rhs: RHS) -> Self::Output;
}

// 同一个类型可以多种方式实现
impl Mul<f64> for Complex { /* ... */ }  // Complex * f64
impl Mul<i32> for Complex { /* ... */ }  // Complex * i32
impl Mul for Complex { /* ... */ }       // Complex * Complex(RHS=Self 默认)

选择指南:如果每个类型最多需要一次实现,用关联类型;如果一个类型需要多次实现(不同参数不同行为),用泛型参数。

子 Trait(Supertrait)

一个 trait 可以要求实现者同时实现其他 trait:

rust
trait Creature: Visible {
    fn position(&self) -> (i32, i32);
    fn facing(&self) -> Direction;
}

// 等价写法:
trait Creature where Self: Visible {
    fn position(&self) -> (i32, i32);
    fn facing(&self) -> Direction;
}

这表示:任何实现 Creature 的类型必须首先实现 Visible。子 trait 的方法可以调用父 trait 的方法:

rust
impl Creature for Broom {
    fn position(&self) -> (i32, i32) {
        (self.x, self.y)
    }
    fn facing(&self) -> Direction {
        Direction::Up  // 扫帚总是朝上
    }
    // Broom 也必须实现 Visible 的所有方法
}

完全限定方法调用

当方法名冲突或类型无法推断时,使用完全限定语法:

rust
// 四种等价的调用方式(从最常用到最显式)
"hello".to_string();                      // 方法调用
ToString::to_string("hello");             // 限定 trait 和类型
<str as ToString>::to_string("hello");    // 完全限定语法
Default::default();                       // 关联函数

完全限定语法在以下场景有用:

  • 两个 trait 提供了同名方法
  • self 参数的类型无法推断
  • 将方法作为函数值传递
rust
// 将 add 作为函数值
let add_fn = <i32 as std::ops::Add>::add;
println!("{} + {} = {}", 3, 4, add_fn(3, 4));

impl Trait

impl Trait 让你在函数签名中用 trait 名称代替具体类型——这既不是 trait 对象也不是显式泛型,而是匿名泛型

rust
// 作为返回类型——不暴露具体类型
fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) -> impl Iterator<Item = u8> {
    v.into_iter()
        .chain(u.into_iter())
        .cycle()
}
// 调用者只知道它是一个 Iterator<Item=u8>,不关心具体类型

// 作为参数类型——等价于泛型
fn print(val: impl Display) {
    println!("{val}");
}
// 等价于:
fn print<T: Display>(val: T) {
    println!("{val}");
}

impl Trait 的关键限制:返回 impl Trait 的函数,所有返回路径必须返回同一种具体类型

rust
// 编译错误!两个分支返回不同类型
fn pick_one(condition: bool) -> impl Display {
    if condition { "hello" } else { 42.to_string() }
    // error: `if` and `else` have incompatible types
}

空白et实现(Blanket Implementation)

泛型 impl 块可以为满足特定条件的所有类型一次性实现 trait:

rust
// 标准库中的空白et实现:任何实现 Display 的类型自动实现 ToString
impl<T: Display> ToString for T {
    fn to_string(&self) -> String {
        use std::fmt::Write;
        let mut buf = String::new();
        write!(buf, "{}", self).unwrap();
        buf
    }
}

// 标准库:任何实现 Write 的类型自动获得 write_all 和 write_fmt
impl<T: Write> Write for &mut T {
    fn write(&mut self, buf: &[u8]) -> Result<usize> {
        (**self).write(buf)
    }
    fn flush(&mut self) -> Result<()> {
        (**self).flush()
    }
}

空白et实现让你定义一次行为,让它自动适用于无数类型——这是 Rust trait 系统最强大的特性之一。

反向推导约束

编写泛型代码时,一个好的实践是从具体函数出发,逐步泛化:

rust
// 第 1 步:具体版本(只处理 i64)
fn dot(v1: &[i64], v2: &[i64]) -> i64 {
    let mut total = 0;
    for i in 0..v1.len() {
        total += v1[i] * v2[i];
    }
    total
}

// 第 2 步:观察所需操作——Add、Mul、Default(初始化 0)
// 第 3 步:检查这些操作的 trait 签名
// 第 4 步:添加约束

fn dot<N>(v1: &[N], v2: &[N]) -> N
where
    N: Add<Output = N> + Mul<Output = N> + Default + Copy,
{
    let mut total = N::default();
    for i in 0..v1.len() {
        total = total + v1[i] * v2[i];
    }
    total
}

这种“反向推导”的泛型编程方式与 C++ 模板形成鲜明对比——C++ 的约束是隐式的(只要模板实例化成功就算通过),而 Rust 的 trait 约束显式声明了类型必须满足的条件,带来更清晰的错误消息。

关联常量

Trait 可以声明关联常量——类型实现 trait 时提供具体值:

rust
trait Float {
    const ZERO: Self;
    const ONE: Self;
    const INFINITY: Self;
    const NEG_INFINITY: Self;
}

impl Float for f32 {
    const ZERO: f32 = 0.0;
    const ONE: f32 = 1.0;
    const INFINITY: f32 = f32::INFINITY;
    const NEG_INFINITY: f32 = f32::NEG_INFINITY;
}

关联常量不能与 trait 对象一起使用——编译器需要知道具体类型才能确定常量值。

trait 是 Rust 的基石

trait 贯穿 Rust 的整个生态系统。几乎每个标准库特性都建立在 trait 之上:

  • IteratorIntoIteratorFromIterator —— 迭代与集合
  • ReadWriteSeek —— I/O
  • AddMulPartialEqOrd —— 运算符
  • CloneCopyDebugDisplay —— 实用 trait
  • FnFnMutFnOnce —— 闭包
  • SendSync —— 并发安全
  • Drop —— 析构

理解 trait 和泛型是以 Rust 思维编程的关键——后续章节关于闭包、迭代器、I/O 和并发的全部内容都将建立在这个基础之上。

与其它语言的对比

概念RustC++JavaHaskell
接口/抽象trait抽象基类/Concepts (C++20)interfacetypeclass
泛型<T: Trait>template<typename T><T extends Interface>参数多态
分发静态分发为主 + dyn 动态分发模板 = 静态;虚函数 = 动态动态分发为主静态分发
约束trait bound 显式声明Concepts (C++20) / 隐式 SFINAEextends + supertypeclass constraint
默认实现trait 默认方法虚函数默认实现default method (Java 8+)默认实现
多实现关联类型 + 泛型 trait模板特化单一实现实例声明
孤儿规则强制(trait 或类型在本 crate)无(任意特化)无(但推荐)

C++20 引入的 Concepts 是向 Rust 的 trait 约束靠拢的一步——它让模板的约束从隐式变为显式,大大改善了错误消息质量。但 Rust 的 trait 系统从一开始就设计为整个语言的基石,更统一和一致。

小结

  • trait 定义能力:类似接口或抽象基类,定义类型必须实现的方法。默认方法减少 boilerplate。子 trait 表达 trait 间的依赖关系。
  • 泛型实现多态:单态化为每个具体类型生成优化代码(静态分发)。dyn Trait 提供动态分发,适合异构集合。
  • trait bound 和 where 子句显式约束类型参数,产生清晰的编译错误消息——与 C++ 模板的隐式约束形成对比。
  • 关联类型适合“一对一”关系(Iterator::Item);泛型 trait 适合“一对多”关系(Mul<RHS>)。
  • 孤儿规则确保 trait 实现的全局唯一性——只能在 trait 或类型的定义 crate 中实现。
  • 空白et实现为满足约束的所有类型自动提供 trait 实现,是 Rust 代码复用的强大工具。
  • impl Trait 作为匿名泛型简化签名,同时保持静态分发。