08
泛型与 Trait:写一次,处处用
Generics · Traits · Trait Bounds
泛型 = 类型参数化(对标 Java 泛型);Trait = 行为契约(对标 Java 接口但更强,能有默认实现)。两者配合就是 Rust 的复用核心。
泛型:函数、结构体、方法
// 泛型函数:T 必须能比较大小(: PartialOrd 是 trait bound)
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut max = &list[0];
for item in list {
if item > max { max = item; }
}
max
}
// 泛型结构体
struct Point<T> { x: T, y: T }
impl<T> Point<T> {
fn x(&self) -> &T { &self.x }
}
fn main() {
let nums = [3, 7, 2];
println!("最大是 {}", largest(&nums));
let p = Point { x: 1, y: 2 };
println!("{}", p.x());
}
论泛型为什么零成本:单态化
Rust 在编译期把泛型展开成具体类型——largest(&[i32]) 和 largest(&[f64]) 各生成一份专属机器码。没有运行时虚表查找,性能跟手写具体类型一模一样。代价是代码体积略增(这就是"零成本抽象"的真正含义)。
Trait:定义一组行为
// 定义 trait:类似接口,可以有默认实现
pub trait Summary {
fn summarize(&self) -> String; // 必须实现的方法
fn summarize_author(&self) -> String {
String::from("(阅读全文…)") // 默认实现,可被覆盖
}
}
struct Article { title: String, author: String }
// 给 Article 实现这个 trait
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} - 作者 {}", self.title, self.author)
}
}
// trait 作为参数:impl Summary 是最简写法
fn notify(item: &impl Summary) {
println!("头条:{}", item.summarize());
}
// 多个 bound 用 + ;复杂时用 where 子句更清爽
fn print_both<T: Summary + std::fmt::Display>(a: T, b: T) {
println!("{} 与 {}", a, b);
}
fn main() {
let art = Article { title: String::from("Rust 入门"), author: String::from("老王") };
notify(&art);
}
$ cargo run
头条:Rust 入门 - 作者 老王
返回 impl Trait 与 trait 对象
// 返回实现了 Summary 的类型(只能返回一种具体类型)
fn make_article() -> impl Summary {
Article { title: String::from("x"), author: String::from("y") }
}
// trait 对象:要存"多种不同类型"时用 Box<dyn Summary>(动态分发)
fn main() {
let _a = make_article();
let items: Vec<Box<dyn Summary>> = vec![]; // 可装不同类型的动态列表
println!("动态列表长度 {}", items.len());
}
| 泛型 / impl Trait(静态分发) | Box<dyn Trait>(动态分发) | |
|---|---|---|
| 原理 | 编译期单态化,生成专属代码 | 运行时查虚表指针 |
| 性能 | 快,可内联优化 | 略慢(多一次间接跳转) |
| 灵活度 | 同一函数只能一种具体类型 | 能装多种类型,列表/容器友好 |
孤儿规则与常用 trait
论孤儿规则(Orphan Rule)
要为一个类型实现 trait,二者至少有一个是你自己定义的。也就是说你不能给 Vec 实现一个别人的 Display——这保证了 trait 不会在不同 crate 里被冲突地实现。想绕过去就用 newtype 模式(第 14 章)。
- 打印:
Debug({:?})、Display({})。 - 比较:
PartialEq / Eq(==)、PartialOrd / Ord(排序)。 - 复制转换:
Clone / Copy / Default / From / Into / AsRef。 - 容器迭代:
Iterator / IntoIterator / Read / Write / Hash。 - 自定义智能指针:
Deref(*解引用)/ Drop(析构)。
常量泛型 const generics:让"数值"也当类型参数
前面泛型传的是类型(T)。可有时候你要把一个具体数值当参数——比如"长度为 N 的数组"。普通泛型做不到,常量泛型(const N: usize)就是干这个的。
// N 是一个编译期确定的整数,不是值,而是类型参数的一部分
struct Buffer<const N: usize> {
data: [f32; N],
}
impl<const N: usize> Buffer<N> {
fn size(&self) -> usize { N }
}
fn main() {
// [f32; 4] 和 [f32; 8] 在类型层面就是两种不同的 Buffer
let a: Buffer<4> = Buffer { data: [0.0; 4] };
let b: Buffer<8> = Buffer { data: [0.0; 8] };
println!("a 长度 {},b 长度 {}", a.size(), b.size());
// 你没法把 a 传给要 Buffer<8> 的函数——编译期就拦住长度不匹配
}
$ cargo run
a 长度 4,b 长度 8
人话:普通数组 [f32; N] 的 N 以前是写死的,现在 N 能泛化。Rust 的定长数组切片、array::from_fn、SIMD 向量化类型都靠它。业务代码里你写得少,但看标准库源码到处是 <const N: usize>。