trait 与共享行为
你会学到什么
trait定义一组类型可以共享的方法(类似接口)。- 为多个类型实现同一个 trait。
- trait 可以提供默认方法,实现者按需覆盖。
- 把 trait 作为泛型函数的约束(trait bound)。
- 参数位置与返回位置的
impl Trait。 - 为自定义类型实现标准库 trait(
std::fmt::Display)。
本章聚焦“静态分发”(编译期单态化)。运行时多态用的 dyn trait 对象留到下一章。
最小示例
trait Summary {
fn summarize(&self) -> String;
fn preview(&self) -> String {
format!("{} (点击查看更多)", self.summarize()) // 默认方法
}
}
运行代码
cd examples
cargo run -p rt_17_traits
cargo test -p rt_17_traits
代码组织
本章示例按职责拆成了多个文件,更贴近真实项目的组织方式:
17_traits/
├── Cargo.toml
└── src/
├── lib.rs # mod 声明 + pub use 重导出 + 库级文档
├── summary.rs # Summary trait、Article/Tweet 及操作它们的函数
├── shape.rs # Shape trait、Circle/Rectangle、Display 实现及相关函数
└── main.rs # 瘦入口:use 库里的类型,跑一遍演示
每个模块都带有自己的单元测试(#[cfg(test)] mod tests),测试就近放在它所覆盖的代码旁边。注意调用 trait 方法时,需要把对应的 trait(如 Summary)引入作用域。
代码讲解
为多个类型实现同一个 trait
Summary 提供了一个必须实现的 summarize,外加两个默认方法 preview 与
word_count。Article 不覆盖任何默认方法,于是自动获得默认实现;Tweet
则覆盖了 preview,因为它本身已经足够简短。默认方法可以调用 self.summarize(),
所以即便实现者只写一行,也能复用完整逻辑。
trait bound:三种等价写法
约束泛型“必须实现某个 trait”有几种写法,挑顺手的即可:
fn announce<T: Summary>(item: &T) -> String { /* 泛型 + trait bound */ }
fn headline(item: &impl Summary) -> String { /* 参数位置 impl Trait,语法糖 */ }
泛型写法 <T: Summary> 能在多个参数间共享同一个 T,表达力最强;&impl Summary
更简洁。两者都是编译期单态化,零运行时开销。
返回位置的 impl Trait
返回 impl Summary 时,调用方只知道“这是一个 Summary”,不必关心具体类型:
fn featured() -> impl Summary {
Tweet { /* ... */ }
}
这在返回闭包、迭代器等“类型名难以书写”的场景特别有用。
为自定义类型实现标准库 trait
Shape 演示了用 trait bound 写通用算法(describe_shape)。同时我们给
Rectangle 实现了标准库的 std::fmt::Display,于是它可以被 {} 直接打印:
impl std::fmt::Display for Rectangle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}×{} 的矩形", self.width, self.height)
}
}
因为 Rectangle 定义在本 crate,满足孤儿规则,所以允许这样实现。
常见错误
试图为外部类型实现外部 trait(孤儿规则):
impl std::fmt::Display for Vec<i32> { /* ❌ 两者都不是本地定义 */ }
trait 或类型至少有一个定义在你自己的 crate 里才允许实现。
练习
- 定义一个
Describetrait,给i32和String各实现一份。 - 再加一个
Triangle实现Shape,并让它也实现Display。 - 写一个返回
impl Shape的函数,根据参数返回圆形或矩形(提示:返回位置的impl Trait要求所有分支是同一具体类型,思考为什么,这正是下一章dyn要解决的问题)。
小结
trait 是 Rust 的抽象基石:定义共享行为、提供默认实现、配合泛型做静态分发。
下一步
当需要在运行时混合多种实现时,要用 trait 对象。下一章学习 dyn。
完整示例代码
下面是 examples/17_traits/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。
examples/17_traits/src/main.rs
//! 演示入口:调用库里拆分好的 `summary` 与 `shape` 模块,跑一遍带标签的输出。
// 注意:调用 trait 方法(如 summarize / area)需要把对应 trait 引入作用域。
use rt_17_traits::{
Article, Circle, Rectangle, Summary, Tweet, announce, describe_shape, featured, headline,
};
fn main() {
let article = Article {
title: "Rust 入门".to_string(),
author: "Alice".to_string(),
};
let tweet = Tweet {
user: "bob".to_string(),
text: "Rust 真香".to_string(),
};
println!("== trait bound 与默认方法 ==");
println!("{}", announce(&article)); // 用默认 preview
println!("{}", announce(&tweet)); // 用覆盖后的 preview
println!("{}", headline(&article)); // impl Trait 参数 + word_count
println!("\n== 返回位置的 impl Trait ==");
let pick = featured();
println!("{}", pick.summarize());
println!("\n== Shape 与 Display ==");
let circle = Circle { radius: 2.0 };
let rect = Rectangle {
width: 3.0,
height: 4.0,
};
println!("{}", describe_shape(&circle));
println!("{}", describe_shape(&rect));
println!("Display: {rect}"); // 直接用 {} 打印
} examples/17_traits/src/lib.rs
//! trait(特征):定义一组类型可以共享的行为,类似其他语言的“接口”。
//!
//! 本章覆盖:
//! - 定义 trait 与“必须实现”的方法;
//! - 为多个类型实现同一个 trait;
//! - 默认方法,以及在实现里覆盖默认方法;
//! - 把 trait 作为泛型函数的约束(trait bound);
//! - 参数位置与返回位置的 `impl Trait`;
//! - 为自定义类型实现标准库 trait(`std::fmt::Display`)。
//!
//! 本章聚焦“静态分发”(编译期单态化);运行时多态用的 `dyn` trait 对象
//! 留到下一章。
//!
//! 代码按职责拆分为两个模块:
//! - [`summary`]:`Summary` trait、`Article`/`Tweet` 及操作它们的函数;
//! - [`shape`]:`Shape` trait、`Circle`/`Rectangle`、`Display` 实现及相关函数。
pub mod shape;
pub mod summary;
pub use shape::{Circle, Rectangle, Shape, describe_shape};
pub use summary::{Article, Summary, Tweet, announce, featured, headline}; examples/17_traits/src/shape.rs
//! 图形相关:`Shape` trait、`Circle`/`Rectangle` 类型、`Display` 实现,
//! 以及操作 `Shape` 的函数。
//!
//! 本模块演示:
//! - 用 trait bound 写通用算法;
//! - 为自定义类型实现标准库 trait(`std::fmt::Display`)。
use std::fmt;
/// 一个带“面积”行为的 trait,演示用 trait bound 写通用算法。
pub trait Shape {
/// 返回图形面积。
fn area(&self) -> f64;
/// 默认方法:名称,默认未知。
fn name(&self) -> &str {
"图形"
}
}
/// 圆形。
pub struct Circle {
pub radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn name(&self) -> &str {
"圆形"
}
}
/// 矩形。
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> &str {
"矩形"
}
}
/// 为自定义类型实现标准库 trait `Display`,让它能被 `{}` 直接打印。
impl fmt::Display for Rectangle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}×{} 的矩形", self.width, self.height)
}
}
/// 泛型函数 + trait bound:对任意 `Shape` 给出一句描述。
pub fn describe_shape<S: Shape>(shape: &S) -> String {
format!("{} 的面积是 {:.2}", shape.name(), shape.area())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shape_area_and_name() {
let rect = Rectangle {
width: 3.0,
height: 4.0,
};
assert_eq!(rect.name(), "矩形");
assert!((rect.area() - 12.0).abs() < 1e-9);
}
#[test]
fn display_for_rectangle() {
let rect = Rectangle {
width: 3.0,
height: 4.0,
};
assert_eq!(format!("{rect}"), "3×4 的矩形");
}
} examples/17_traits/src/summary.rs
//! 摘要相关:`Summary` trait、`Article`/`Tweet` 类型,以及操作 `Summary` 的函数。
//!
//! 本模块演示:
//! - 定义 trait 与“必须实现”的方法;
//! - 默认方法,以及在实现里覆盖默认方法;
//! - 把 trait 作为泛型函数的约束(trait bound);
//! - 参数位置与返回位置的 `impl Trait`。
/// 一个描述“可被摘要”行为的 trait。
pub trait Summary {
/// 必须由实现者提供:返回内容的一行摘要。
fn summarize(&self) -> String;
/// 默认方法:在摘要后追加提示语。
///
/// 实现者若不覆盖就直接复用这份实现,因此它能调用 `self.summarize()`。
fn preview(&self) -> String {
format!("{} (点击查看更多)", self.summarize())
}
/// 默认方法:统计摘要里的词数(按空白切分)。
fn word_count(&self) -> usize {
self.summarize().split_whitespace().count()
}
}
/// 文章:拥有标题与作者。
pub struct Article {
pub title: String,
pub author: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} - {}", self.title, self.author)
}
// 不覆盖 preview / word_count,直接使用默认实现。
}
/// 推文:拥有用户名与正文。
pub struct Tweet {
pub user: String,
pub text: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.user, self.text)
}
/// 覆盖默认实现:推文本身已足够简短,预览就等于摘要。
fn preview(&self) -> String {
self.summarize()
}
}
/// 用泛型 + trait bound 接受任意实现了 `Summary` 的类型。
///
/// `<T: Summary>` 与参数位置的 `&impl Summary` 等价,但泛型写法可以在多个
/// 参数间共享同一个类型 `T`,表达力更强。
pub fn announce<T: Summary>(item: &T) -> String {
format!("最新: {}", item.preview())
}
/// 参数位置的 `impl Trait`:是上面泛型写法的“匿名”语法糖,更简洁。
pub fn headline(item: &impl Summary) -> String {
format!("[{} 词] {}", item.word_count(), item.summarize())
}
/// 返回位置的 `impl Trait`:调用方只知道“这是一个 Summary”,
/// 不必关心具体类型。适合返回闭包或难以书写的复杂类型。
pub fn featured() -> impl Summary {
Tweet {
user: "rustlang".to_string(),
text: "Rust 2024 发布啦".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uses_default_preview() {
let article = Article {
title: "T".to_string(),
author: "A".to_string(),
};
assert_eq!(article.preview(), "T - A (点击查看更多)");
}
#[test]
fn overrides_preview() {
let tweet = Tweet {
user: "u".to_string(),
text: "hi".to_string(),
};
assert_eq!(tweet.preview(), "@u: hi");
}
#[test]
fn default_word_count() {
let tweet = Tweet {
user: "u".to_string(),
text: "hello world".to_string(),
};
// summarize 为 "@u: hello world",按空白切分得 3 个词。
assert_eq!(tweet.word_count(), 3);
}
#[test]
fn returned_impl_trait_summarizes() {
let item = featured();
assert_eq!(item.summarize(), "@rustlang: Rust 2024 发布啦");
}
} examples/17_traits/Cargo.toml
[package]
name = "rt_17_traits"
version.workspace = true
edition.workspace = true
publish.workspace = true