泛型:写一遍,用千遍
写一个取两数较大者的函数:
fn max_i32(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
明天要比较 f64,难道抄一份 max_f64?当然不。把类型变成一个「参数」:
fn max<T>(a: T, b: T) -> T {
if a > b { a } else { b }
} // 等等——这样编译不过!
<T> 读作「本函数适用于任意类型 T」,调用时编译器自动把 T 落实成具体类型。但上面这段被拒了,理由发人深省:你凭什么保证 T 这种类型能比较大小?万一 T 是个 User,两个 User 谁大?「任意类型都行」是一个太大的承诺,编译器要求你把承诺缩小到有据可依。这就引出了本章真正的主角。
trait:一份能力合同
trait(读 /treɪ/,「特质」)是 Rust 的能力合同——大白话:一份「会做什么」的清单。比如标准库里的 PartialOrd 这份合同,内容就是「我能比大小」。任何类型签了这份合同(实现了这个 trait),就获得了比较能力。给泛型加上「只收签了某合同的类型」,叫trait 约束(trait bound):
fn max<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
max(3, 5); // i32 签了 PartialOrd,行
max(2.1, 9.9); // f64 也签了,行
读法:<T: PartialOrd> = 「任意类型 T,前提是它签了 PartialOrd 合同」。承诺收窄了,代码合法了。i32、f64、char、String 都签了这份合同,所以这一个 max 统统适用。
自己定义合同,自己签
合同也能自己写。比如「能被概括成一句话」:
trait Summary {
fn summarize(&self) -> String;
}
让第七章的 User 签这份合同:
impl Summary for User {
fn summarize(&self) -> String {
format!("{},{} 岁", self.name, self.age)
}
}
从此 User 可以出现在任何「要求会概括」的地方。对比传统面向对象的接口(interface),trait 有一个杀手锏:你可以给别人的类型签自己的合同——给标准库的 String 实现你自己写的 trait,完全合法。类型和能力的解绑,让库与库之间的协作灵活得多(这就是第七章说的「松」预留的余地)。
derive:常见合同让编译器代签
有些合同内容机械到编译器能替你写。在结构体头上一行 #[derive(...)] 即可:
#[derive(Debug, Clone)]
struct User {
name: String,
age: u32,
}
let u = User { name: String::from("阿蟹"), age: 27 };
println!("{:?}", u); // Debug 合同:允许用 {:?} 打印内部结构,调试神器
最常用的几份现成合同:Debug(能被 {:?} 打印)、Clone(能深拷贝,第四章的 .clone() 就来自它)、PartialEq(能用 == 比相等)、Default(有默认值)。
零成本抽象:泛型为什么不慢
学到这里该有的疑虑:一个 max 适配所有类型,运行时是不是要做类型判断、付出性能代价?答案是零代价,机制叫单态化(monomorphization)——大白话:编译期,编译器为你实际用到的每种类型各生成一份专属代码。你用了 max(i32) 和 max(f64),编译产物里就躺着两份写死的、各自优化过的 max_i32 和 max_f64,和你手写两份一模一样快。代价是编译慢一点、二进制大一点——成本全部付在编译期,运行时分文不取。
这就是 Rust 的招牌承诺 zero-cost abstractions(零成本抽象)的运作原理,第十章的迭代器流水线靠的也是它:优雅的写法在编译期被「展开」成和你手写一样的基础操作。抽象是给人看的,不是给机器执行的。
拼图齐了
回看全书主线:所有权和借用保证安全(四到六章),结构体和枚举组织数据(七、八章),Result 让失败走明路(九章),泛型和 trait 让代码一次写好、处处适用且零成本(本章)。还剩最后一块最硬的应用场景:并发。多个线程同时扑向同一份数据,是无数语言的事故高发区——而 Rust 的宣传语偏偏叫「无畏并发」(fearless concurrency)。它哪来的底气?终章揭晓。