Rust:trait系统深入理解

Rust 的 trait 是整个类型系统的核心抽象机制,理解 trait 就理解了 Rust 大半的设计哲学。本文从基础定义到高级用法,系统梳理 trait 的各个方面。

trait 定义与实现

trait 定义了一组方法签名,任何类型都可以为其提供实现:

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

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

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

注意一个重要的规则——孤儿规则(orphan rule):你只能在当前 crate 中为类型实现 trait,前提是 trait 或类型至少有一个是在当前 crate 定义的。这防止了不同 crate 之间的 impl 冲突。

默认方法

trait 方法可以有默认实现,实现者可以选择覆盖:

trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

struct Tweet {
    username: String,
    content: String,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
    // summarize() 使用默认实现
}

默认方法可以调用同一 trait 中的其他方法(包括没有默认实现的),这是一种模板方法模式。

Trait Bound

当函数参数需要约束泛型类型时,trait bound 是最常用的手段:

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

// 完整的 trait bound 写法
fn notify<T: Summary>(item: &T) {
    println!("Breaking: {}", item.summarize());
}

// 多个 trait bound
fn notify_and_display<T: Summary + std::fmt::Display>(item: &T) {
    println!("{}: {}", item, item.summarize());
}

// where 子句——bound 太长时更清晰
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Summary + Clone,
    U: std::fmt::Display + std::fmt::Debug,
{
    // ...
    0
}

trait bound 的本质是在编译期进行单态化(monomorphization),编译器为每个具体类型生成专门的函数版本,因此没有运行时开销。

impl Trait 语法

impl Trait 除了用在参数位置,还能用在返回值位置:

fn make_summarizable() -> impl Summary {
    Article {
        title: String::from("Rust is awesome"),
        author: String::from("ferris"),
        content: String::from("..."),
    }
}

但要注意,返回 impl Trait 时只能返回单一具体类型。下面这样是编译不过的:

// 编译错误!返回了两种不同的类型
fn make_summarizable(switch: bool) -> impl Summary {
    if switch {
        Article { /* ... */ }
    } else {
        Tweet { /* ... */ }
    }
}

想返回不同类型,就需要 trait 对象了。

Trait 对象(dyn Trait)

trait 对象通过 dyn 关键字实现动态派发:

fn print_summary(item: &dyn Summary) {
    println!("{}", item.summarize());
}

fn main() {
    let article = Article {
        title: "Ownership".into(),
        author: "ferris".into(),
        content: "...".into(),
    };
    let tweet = Tweet {
        username: "rustlang".into(),
        content: "Rust 1.56 released!".into(),
    };

    // 同一个函数,接受不同类型
    print_summary(&article);
    print_summary(&tweet);

    // 也可以存在集合中
    let items: Vec<Box<dyn Summary>> = vec![
        Box::new(article),
        Box::new(tweet),
    ];
    for item in &items {
        println!("{}", item.summarize());
    }
}

dyn Trait 的底层实现是一个胖指针,包含数据指针和 vtable 指针。vtable 中存放了该类型对这个 trait 所有方法的函数指针。

对象安全(object safety):不是所有 trait 都能做 trait 对象。trait 必须满足:

  • 所有方法的返回类型不能是 Self
  • 所有方法不能有泛型参数

比如 Clone 就不是对象安全的,因为 clone(&self) -> Self 返回了 Self

常用标准库 trait

Display 和 Debug

use std::fmt;

struct Point {
    x: f64,
    y: f64,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

// Debug 通常用 derive
#[derive(Debug)]
struct Rect {
    top_left: Point,
    bottom_right: Point,
}

Display 用于面向用户的输出({}),Debug 用于调试输出({:?})。Debug 几乎总是通过 #[derive(Debug)] 来实现。

Clone 和 Copy

#[derive(Clone)]
struct Buffer {
    data: Vec<u8>,
}

// Copy 要求类型同时实现 Clone,且所有字段都是 Copy 的
#[derive(Clone, Copy)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
}

Copy 表示按位复制语义,赋值时不会发生 move。只有不拥有堆资源的类型才能实现 Copy——VecString 这些都不行。

Iterator

struct Counter {
    count: u32,
    max: u32,
}

impl Counter {
    fn new(max: u32) -> Self {
        Counter { count: 0, max }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let counter = Counter::new(5);
    let sum: u32 = counter.filter(|x| x % 2 == 0).sum();
    println!("sum of evens: {}", sum); // 6 (2 + 4)
}

Iterator 只需要实现 next() 方法,就能免费获得 mapfilterfoldcollect 等几十个方法——这就是默认方法的威力。

From 和 Into

struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

fn main() {
    let boiling = Celsius(100.0);
    let f: Fahrenheit = boiling.into(); // 自动获得 Into
    let f2 = Fahrenheit::from(Celsius(0.0));
}

实现 From 会自动获得 Into,一般只实现 From 就够了。

进阶:Supertraits

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

trait PrettyPrint: fmt::Display {
    fn pretty_print(&self) {
        let output = format!("=== {} ===", self);
        println!("{}", output);
    }
}

PrettyPrint 的实现者必须先实现 Display,这样在 pretty_print 中就能直接用 {} 格式化 self

小结

trait 在 Rust 中承担的角色远比其他语言的 interface 更重。它是泛型约束、多态、运算符重载、类型转换的统一机制。理解了 trait bound 和 trait object 的区别(静态派发 vs 动态派发),就掌握了 Rust 抽象的两条核心路径。