6.4 trait 作为参数与返回值
在 Rust 中,trait 不仅用于定义行为,还可以作为函数参数或返回值的类型,从而实现抽象和多态。Rust 提供了两种主要方式来达成这一目的:impl Trait 和 dyn Trait。它们分别对应编译时多态(静态分发)和运行时多态(动态分发),适用于不同场景。
使用 impl Trait 作为参数
从 Rust 1.0 起,你可以通过泛型 + trait bound 的方式接受实现了特定 trait 的任意类型:
fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}
Rust 1.26 引入了更简洁的 impl Trait 语法,等价写法为:
fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}
这种方式在参数位置使用时,本质上是泛型的语法糖。编译器会对每个具体调用生成特化代码(单态化),因此性能高,但会增加二进制体积。
注意:
impl Trait在参数位置 ≈ 泛型 + trait bound。
使用 impl Trait 作为返回值
impl Trait 更常用于返回值,表示“返回某个实现了该 trait 的具体类型,但不暴露其真实类型”:
fn create_summary() -> impl Summary {
Tweet {
username: String::from("rustlang"),
content: String::from("Rust 1.78 released!"),
}
}
这里,函数返回一个 Tweet,但调用者只知道它实现了 Summary,无法访问 Tweet 的其他字段或方法。这种方式:
- 隐藏实现细节;
- 允许未来更换返回类型而不破坏 API;
- 保持零成本抽象(因为返回的是具体类型,非指针)。
但限制是:函数内部所有 return 路径必须返回同一具体类型。例如,不能有时返回 Tweet,有时返回 NewsArticle。
使用 dyn Trait 实现运行时多态
当需要在运行时处理多种不同类型的值(例如存放在 Vec 中),就必须使用 trait 对象(trait object),即 dyn Trait。
例如:
let tweet = Tweet { /* ... */ };
let article = NewsArticle { /* ... */ };
let summaries: Vec<&dyn Summary> = vec![&tweet, &article];
这里,&dyn Summary 是一个 trait 对象,包含:
- 指向实际数据的指针;
- 指向虚表(vtable)的指针,用于在运行时查找方法实现。
使用 trait 对象时,方法调用通过动态分发完成,有轻微运行时开销,但允许异构集合。
注意:要成为 trait 对象,trait 必须是“对象安全”的(object safe),即不能包含以下内容:
- 方法返回
Self; - 方法包含泛型参数;
- 关联类型未被完全指定。
impl Trait 与 dyn Trait 对比
| 特性 | impl Trait |
dyn Trait |
|---|---|---|
| 分发方式 | 静态(编译时) | 动态(运行时) |
| 性能 | 零开销,可内联 | 有虚表查找开销 |
| 类型信息 | 编译器知道具体类型 | 运行时只知道 trait |
| 返回多个不同类型 | 不支持 | 支持(通过引用或 Box) |
| 存储在集合中 | 不能(因类型不同) | 可以(如 Vec<Box<dyn Trait>>) |
示例:使用 Box<dyn Trait> 存储所有权:
let summaries: Vec<Box<dyn Summary>> = vec![
Box::new(tweet),
Box::new(article),
];
小结
impl Trait 和 dyn Trait 为 Rust 提供了灵活的抽象手段。前者适用于性能敏感、类型统一的场景;后者适用于需要运行时多态或处理异构数据的场合。合理选择两者,既能保持代码的通用性,又能兼顾效率与表达力,这也是编写高质量 Rust 代码的一项重要能力。