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——Vec、String 这些都不行。
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() 方法,就能免费获得 map、filter、fold、collect 等几十个方法——这就是默认方法的威力。
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 抽象的两条核心路径。