Rust Trait:用共享行为连接不同类型
Trait 描述一组类型必须提供的行为,作用接近其他语言的接口,但 Rust 还可以通过默认实现、泛型约束和 trait object 支持静态与动态分发。
1. 定义并实现 Trait
下面的 Describe 要求类型提供 describe 方法。Teacher 和 Student 的字段不同,但都可以被同一个泛型函数使用:
trait Describe {
fn describe(&self) -> String;
}
struct Teacher {
name: String,
subject: String,
}
struct Student {
name: String,
grade: u8,
}
impl Describe for Teacher {
fn describe(&self) -> String {
format!("教师 {},教授 {}", self.name, self.subject)
}
}
impl Describe for Student {
fn describe(&self) -> String {
format!("学生 {},年级 {}", self.name, self.grade)
}
}
fn print_description<T: Describe>(value: &T) {
println!("{}", value.describe());
}
fn main() {
let teacher = Teacher { name: "Lin".into(), subject: "Rust".into() };
let student = Student { name: "Yu".into(), grade: 2 };
print_description(&teacher);
print_description(&student);
}
T: Describe 是编译期约束。编译器会为每种具体类型生成调用路径,通常可以内联,称为静态分发。
2. impl Trait 与 dyn Trait
参数位置可以写成 impl Trait:
#![allow(unused)]
fn main() {
fn print_description(value: &impl Describe) {
println!("{}", value.describe());
}
}
它表达“传入某个实现了 Trait 的具体类型”,仍然是静态分发。等价的泛型写法 fn print_description<T: Describe>(value: &T) 更适合同时约束多个参数为同一种类型。
如果需要在同一个集合中保存不同的实现,使用 trait object:
#![allow(unused)]
fn main() {
let people: Vec<Box<dyn Describe>> = vec![
Box::new(Teacher { name: "Lin".into(), subject: "Rust".into() }),
Box::new(Student { name: "Yu".into(), grade: 2 }),
];
for person in people {
println!("{}", person.describe());
}
}
dyn Describe 通过虚表在运行时选择实现,属于动态分发。Trait 必须满足 object safety 才能作为 trait object;带有返回 Self 或泛型方法的 Trait 通常不能直接用于 dyn Trait。
3. 默认实现与 Trait bound
Trait 可以提供默认实现,具体类型只覆盖真正不同的行为:
#![allow(unused)]
fn main() {
trait Summary {
fn title(&self) -> &str;
fn preview(&self) -> String {
format!("摘要:{}", self.title())
}
}
fn show<T: Summary>(item: &T) {
println!("{}", item.preview());
}
}
多个约束可以使用 + 组合,例如 T: Summary + Display。返回值位置可以写 impl Summary,但一个函数只能返回一种隐藏的具体类型;如果需要返回多种实现,应返回 Box<dyn Summary>。
4. 常见误区
- Trait 不会自动为类型实现行为,必须写出
impl Trait for Type。 impl Trait通常表示一个具体类型;dyn Trait才表示运行时可替换的多种类型。- 方法命名应遵循 Rust 风格,例如
say_age和self_introduction,不要使用Sayage、SelfIntroduction这类大驼峰方法名。 - 只有在确实需要异构集合或运行时插件时才使用
Box<dyn Trait>;普通参数优先使用泛型约束。