10|Trait 与泛型:按能力组织接口
目标:定义一个小型 trait,并让泛型函数处理具有相同行为的不同类型。
假设屏幕上既要显示文本统计,也要显示输入文件名。两者字段不同,却都能提供一行描述。复制两个展示函数会重复行为;强行让它们塞进同一个大结构体,又会出现很多不相关字段。
trait 描述类型需要提供的能力;impl Trait for Type 把能力落实到具体类型。泛型函数中的 T: Describe 表示调用者可以选择不同的具体类型,但它必须实现 Describe。编译器在编译时检查约束,调用者不能靠“刚好有一个同名方法”自动满足 trait。
先抽象真实存在的共同操作。只有一个报告类型时,直接定义方法通常更清晰;本课为了练习泛型,特意构造两个消费者。
两种数据,一种展示能力
创建 examples/traits.rs:
trait Describe {
fn describe(&self) -> String;
}
struct WordCount {
words: usize,
}
struct InputName {
name: String,
}
impl Describe for WordCount {
fn describe(&self) -> String {
format!("words={}", self.words)
}
}
impl Describe for InputName {
fn describe(&self) -> String {
format!("input={}", self.name)
}
}
fn show<T: Describe>(value: &T) {
println!("{}", value.describe());
}
fn main() {
let count = WordCount { words: 2 };
let input = InputName {
name: String::from("notes.txt"),
};
show(&count);
show(&input);
}
运行前先预测:show 的两次调用中,T 是否必须是同一种类型?
cargo run --quiet --example traits
预期输出:
words=2
input=notes.txt
每次调用都可以选择不同的 T。这里使用泛型静态分发,编译器可为所需的具体类型生成对应代码。trait 本身不保存字段,也没有把两个结构体转换成某个共同父类。describe(&self) 只借用实例,两次展示不会消耗数据。
改实现,观察接口保持稳定
- 只把
WordCount的格式字符串改成word count: {}。 - 运行,第一行应变成
word count: 2,第二行不变。 - 检查
show是否完全不需要修改,然后恢复原格式。
独立练习:新增 ByteCount { bytes: usize },实现同一个 trait,让 show(&ByteCount { bytes: 12 }) 输出 bytes=12。
参考扩展与检查
在原示例 main 之前插入这段完整类型与实现;它依赖前面已经定义的 Describe:
struct ByteCount {
bytes: usize,
}
impl Describe for ByteCount {
fn describe(&self) -> String {
format!("bytes={}", self.bytes)
}
}
在原 main 末尾加入 show(&ByteCount { bytes: 12 });。运行同一命令,第三行应为 bytes=12。再加入 assert_eq!(ByteCount { bytes: 12 }.describe(), "bytes=12");,让输出契约成为可执行检查。
如果调用 show 时出现 trait bound 不满足,检查是否真的写了 impl Describe for ...;单独写在结构体自身 impl 中的同名方法不算实现 trait。如果方法签名不匹配,逐项比较参数借用形式和返回类型。需要把不同具体类型放进同一个容器时,可以继续学习 dyn Trait;本课的泛型函数只处理每次调用中的一个具体类型。
本课新增 examples/traits.rs。最终项目的测试将使用标准 trait 的自动派生,例如 Debug 负责调试展示、PartialEq 负责相等比较;无需为只有一种报告的 CLI 增加额外抽象层。