楼层: 首页/ 软件技术/ Rust 语言基础/ 泛型与 Trait:写一次,处处用
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()); }
静态分发 vs 动态分发
泛型 / 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>。