8.1 这节课解决什么问题
你已经隐隐用过很多"魔法":#[derive(Debug)] 让结构体能打印、#[derive(Error)] 自动实现 Display、String + &str 能相加、for x in vec 能遍历、?.into() 能转错误类型。它们的共同幕后主角就是 trait(特质,类似其他语言的接口/协议)。
本课把这些魔法"解密":
rust
trait 定义"一组能力/行为",类型实现它 = 承诺拥有这些能力
泛型 写一次代码,适用于任意实现了某 trait 的类型
trait 对象 让不同类型的值能以"同一个接口"放进同一种容器(动态分发)
一句话总结 Rust 的抽象哲学:凡是"可能不同、但要统一调用"的东西,都用 trait 表达;编译期能确定的用泛型(零成本),运行期才确定的用 trait 对象(vtable)。
8.2 泛型:类型也能是"参数"
8.2.1 泛型函数与结构体
rust
// 泛型函数:T 是"类型参数",调用时编译器自动推断/填充
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut biggest = &list[0];
for item in list {
if item > biggest {
biggest = item;
}
}
biggest
}
// 泛型结构体:字段可以是任意类型 X、Y
struct Pair<X, Y> {
first: X,
second: Y,
}
fn main() {
let numbers = vec![3, 9, 4, 1];
println!("最大数:{}", largest(&numbers)); // 9
let words = vec![String::from("apple"), String::from("zoom")];
println!("最大串:{}", largest(&words)); // zoom
let p = Pair { first: 1u32, second: String::from("one") };
println!("{} {}", p.first, p.second);
}
<T>里的T在调用时被具体类型替换;largest<T: PartialOrd>中的T: PartialOrd是泛型约束 :要求 T 支持>比较。不写这个约束,item > biggest会报"无法比较"(编译器不知道任意 T 都能比大小)------约束就是把"这个函数需要什么能力"告诉编译器。
8.2.2 impl<T>:给"任何 T"实现方法
rust
struct Pair<X, Y> { first: X, second: Y }
impl<X, Y> Pair<X, Y> {
fn new(first: X, second: Y) -> Self { // Self = Pair<X, Y>
Pair { first, second }
}
}
// 只对特定组合生效的方法
impl Pair<u32, String> {
fn describe(&self) -> String {
format!("数字 {} 和 {}", self.first, self.second)
}
}
fn main() {
let p = Pair::new(1u32, String::from("一"));
println!("{}", p.describe());
}
8.2.3 单态化(monomorphization):泛型是"零成本"的
编译时,编译器会把每个具体调用生成一份专用代码(如 largest<&[i32]>、largest<&[String]> 各一份),运行期没有任何间接跳转------这就是"泛型零成本抽象"。代价是编译产物变大(每一份具体化都占空间),所以不要滥用深层泛型嵌套。
8.3 trait:定义与实现一组能力
8.3.1 最小可运行示例
rust
trait Summarizable {
fn summary(&self) -> String;
}
struct Article {
title: String,
author: String,
}
struct Tweet {
text: String,
likes: u32,
}
// 为 Article 实现 trait
impl Summarizable for Article {
fn summary(&self) -> String {
format!("《{}》by {}", self.title, self.author)
}
}
// 为 Tweet 实现 trait
impl Summarizable for Tweet {
fn summary(&self) -> String {
format!("(赞{}) {}", self.likes, self.text)
}
}
fn main() {
let a = Article { title: String::from("Rust 入门"), author: String::from("小灵") };
let t = Tweet { text: String::from("今天写完了 FFI 课"), likes: 42 };
println!("{}", a.summary());
println!("{}", t.summary());
}
要点:
trait X { fn m(&self) -> T; }只声明签名,不写实现;impl X for MyType { ... }为类型补实现;- 孤儿规则 :
impl时 trait 或类型至少有一个是"本 crate 定义"的(不能给第三方类型实现第三方 trait,避免冲突)。这一点在实战里会管着你"能不能给 u32 加方法"。
8.3.2 标准库高频 trait 一览(逐个给最小用法)
| trait | 能力 | 最小演示 |
|---|---|---|
Display |
用 {} 打印 |
impl fmt::Display |
Debug |
用 {:?} 打印(可 derive) |
#[derive(Debug)] |
PartialEq |
==/!=(可 derive) |
#[derive(PartialEq)] |
Eq |
完整相等语义(要求 PartialEq) | derive 时带上 |
Clone |
.clone() 深拷贝(可 derive) |
#[derive(Clone)] |
Copy |
赋值即复制(只对纯栈类型) | #[derive(Copy, Clone)] |
PartialOrd/Ord |
</> 与排序 |
derive 或手写 |
Default |
.default() 提供默认实例 |
#[derive(Default)] |
Hash |
可放进 HashMap/HashSet | #[derive(Hash)] |
From<T> |
T::from(x) / x.into() |
impl From<A> for B |
Error |
错误类型(配 thiserror) | 见 7 课 |
Iterator |
可被 for 消费 | 见 9 课 |
Display + Error |
自定义错误 | thiserror |
rust
#[derive(Debug, Clone, PartialEq, Default)]
struct Profile {
name: String,
age: u8,
}
fn main() {
let p = Profile::default(); // Default
let p2 = p.clone(); // Clone
println!("{:?} 相等?{}", p, p == p2); // Debug / PartialEq
}
8.3.3 默认方法:trait 可以带实现
rust
trait Greeter {
fn name(&self) -> String;
// 默认实现:用 name() 拼一句话(实现者可覆写)
fn greet(&self) -> String {
format!("你好,{}!", self.name())
}
}
struct Dog;
impl Greeter for Dog {
fn name(&self) -> String { String::from("旺财") }
// greet 用默认实现
}
struct Robot;
impl Greeter for Robot {
fn name(&self) -> String { String::from("R2") }
fn greet(&self) -> String { format!("<机械音> {} 启动", self.name()) }
}
fn main() {
println!("{}", Dog.greet());
println!("{}", Robot.greet());
}
8.4 用 trait 写约束:T: Trait 与 where
泛型 + trait 约束 = "这段代码适用于一切具有某能力的类型":
rust
use std::fmt::Display;
// 写法一:内联约束
fn announce<T: Display>(item: T) -> String {
format!("📢 {}", item)
}
// 写法二:where 子句(约束多时更易读)
fn announce2<T, U>(a: T, b: U) -> String
where
T: Display + Clone,
U: Display,
{
let copy = a.clone(); // 证明约束生效:能用 clone 因为 T: Clone
format!("{} & {} (备份:{})", a, b, copy)
}
fn main() {
println!("{}", announce(42));
println!("{}", announce2("Rust", 2026));
}
💡 trait bound 也能当参数类型 :
fn greet(g: &impl Greeter)等价于fn greet<T: Greeter>(g: &T)(impl Trait 语法糖,9 课闭包参数里大量出现)。
8.4.1 返回实现某 trait 的类型
rust
fn make_greeter() -> impl Greeter {
Robot
}
// 用 impl Trait 当返回值:只告诉调用者"它 Greeter",不暴露具体类型(常用于返回迭代器/闭包)
8.5 关联类型与 Self
一些 trait 需要"随实现者而变的配套类型",用关联类型表达:
rust
// 简化版 Iterator(标准库签名)
pub trait Iterator {
type Item; // 关联类型:每个实现者声明"我产出什么"
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter { count: u32 }
impl Iterator for Counter {
type Item = u32; // 指明 Item = u32
fn next(&mut self) -> Option<u32> {
self.count += 1;
if self.count < 6 { Some(self.count) } else { None }
}
}
fn main() {
let mut c = Counter { count: 0 };
// for 循环内部就是反复调用 next()
while let Some(n) = c.next() {
print!("{n} ");
}
println!(); // 1 2 3 4 5
}
Self::Item表示"当前实现者的 Item";- 关联类型 vs 泛型参数的选择:一个实现只有一种配套类型 → 关联类型;需要"一个实现适配多种配套类型"才用泛型参数。
8.6 trait 对象 dyn Trait:把不同类型装进同一个集合
泛型在编译期展开,意味着 Vec<impl Summarizable> 做不到------每个元素类型得一样。若要运行期多态(不同类型混在一起、统一调用),用 trait 对象:
rust
trait Sound {
fn play(&self) -> String;
}
struct Dog;
impl Sound for Dog {
fn play(&self) -> String { String::from("汪汪") }
}
struct Cat;
impl Sound for Cat {
fn play(&self) -> String { String::from("喵喵") }
}
fn main() {
// Vec<Box<dyn Sound>>:堆上存各类型的实例,以"Sound"为统一接口
let zoo: Vec<Box<dyn Sound>> = vec![Box::new(Dog), Box::new(Cat)];
for animal in zoo {
println!("{}", animal.play());
}
}
要点与代价:
dyn Trait必须放在指针后面 (&dyn Sound、Box<dyn Sound>),因为 trait 对象大小未知,只能靠引用/指针指向堆上的具体对象;- 每个对象额外携带一个 vtable(虚函数表) ,调用
play()通过表间接跳转 → 比泛型的直接调用慢一丁点、且不能内联; - 对象安全要求:trait 的方法不能用泛型参数(会破坏 vtable 布局),本章的 trait 都能用。
| 泛型(静态分发) | dyn Trait(动态分发) |
|
|---|---|---|
| 决策时机 | 编译期 | 运行期(vtable 跳转) |
| 性能 | 可内联、最快 | 略慢,有间接调用 |
| 同一容器放多类型 | 不能 | 能 |
| 适用 | 数量少、类型编译期已知 | 插件化、事件/命令集合、跨边界 |
💡 实战判断:第 19 课做"命令分发"时,若命令类型在编译期可穷举 →
match enum(类型安全、最快);若要做"可插拔的能力清单" →Vec<Box<dyn Handler>>。二者都会用到,本课先建立概念。
8.7 #[derive(...)] 派生宏速查表
| 派生 | 生成的能力 | 要求 |
|---|---|---|
Debug |
{:?} |
无 |
Clone |
.clone() |
所有字段 Clone |
Copy |
赋值即复制 | 所有字段 Copy(不能同时有 String/Vec) |
PartialEq, Eq |
==/!= |
字段可比 |
PartialOrd, Ord |
排序比较 | 字段可排序 |
Hash |
哈希 | 字段可哈希 |
Default |
默认值 | 字段有默认值 |
派生是"我能信任编译器生成、与手工实现语义一致"时的首选;需要特殊语义(如 PartialEq 忽略某字段、newtype 比较)时再手写。
8.8 📝 动手练习
参考实现放 code/08-traits-generics/(写作时同步给出)。
- largest 通用化 :用 8.2.1 的
largest测试f64、char各一次;尝试对Vec<String>去掉PartialOrd约束看报错(体会约束的意义)。 - 几何接口 :定义
trait Area { fn area(&self) -> f64; },为Rectangle、Circle实现;再写fn total_area(list: &[&dyn Area]) -> f64做动态分发(先收集let shapes: Vec<&dyn Area> = vec![&rect, &circle];再传入),验证混装不同类型能统一调用。 - Display 手写 :不 derive,手写
impl Display给第 6 课的User,让println!("{user}")输出小灵(vip)样式。 - 约束函数 :写
fn describe<T: Display + Clone>(t: &T) -> String,内部 clone 一份再打印;测试传i32、String、u32。 - From 练习 :
struct Celsius(f64); struct Fahrenheit(f64);,为两边互写From,验证.into()双向可用。 - 默认方法覆写 :把 8.3.3 的 Greeter 抄下,新增
struct Cat;实现Greeter(name 返回"咪咪",greet 覆写成"喵!")。 - trait 对象 vs 枚举(思考题) :给"事件"建模------方案 A
enum Event {...},方案 Btrait Event + Box<dyn Event>。写出两者各自的取舍 3 条,并说出实战 19 课"UI 事件"你会选哪个、为什么(答案在第 19 课揭晓)。
验收门禁 :能解释 trait 是什么、impl/孤儿规则;能写 T: Trait 约束与 where;能说清单态化与 vtable 的区别与各自适用;会查 derive 表给结构体补派生。
✅ 本节小结
- 泛型 :类型参数
T;impl<T>;约束T: Trait/where;编译期单态化(零成本、产物变大); - trait :定义能力 →
impl Trait for Type;孤儿规则;标准库高频 trait(Display/Debug/Clone/PartialEq/Default/From...);默认方法; - impl Trait:参数与返回位置的语法糖;
- 关联类型 :
type Item------"实现者配套类型"用关联类型; - trait 对象
dyn Trait:放指针后、vtable 动态分发、可混装不同类型,泛型方法不可出现在对象安全 trait 里; - 派生宏:编译器替写样板;字段要求不符会编译报错。
下一课预告 :第 9 课《集合、迭代器与闭包》------HashMap 等容器全家桶 + Rust 最优雅的"数据管道":迭代器链式操作(map/filter/fold...)与闭包捕获。第 8 课的 Iterator 只是开头,这课把它用到极致,并顺带揭秘"为什么 Rust 处理集合这么顺手"。