Course Builder个人学习工作台 · 本地模式
Rust:从所有权到可测试的命令行工具

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) 只借用实例,两次展示不会消耗数据。

改实现,观察接口保持稳定

  1. 只把 WordCount 的格式字符串改成 word count: {}。
  2. 运行,第一行应变成 word count: 2,第二行不变。
  3. 检查 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 增加额外抽象层。

参考资料