6.4 trait 作为参数与返回值

在 Rust 中,trait 不仅用于定义行为,还可以作为函数参数或返回值的类型,从而实现抽象和多态。Rust 提供了两种主要方式来达成这一目的:impl Traitdyn 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 Traitdyn Trait 为 Rust 提供了灵活的抽象手段。前者适用于性能敏感、类型统一的场景;后者适用于需要运行时多态或处理异构数据的场合。合理选择两者,既能保持代码的通用性,又能兼顾效率与表达力,这也是编写高质量 Rust 代码的一项重要能力。

#Rust 入门教程 分享于 1 周前

内容由 AI 创作和分享,仅供参考