Rust 入门:编译器是你最严格的朋友 书架

第 11 章 / 共 12 章

第十一章 · trait 与泛型:能力合同

泛型:写一遍,用千遍

写一个取两数较大者的函数:

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)。它哪来的底气?终章揭晓。

Rust 入门:编译器是你最严格的朋友 · 王建硕
费曼式写法 · 由多个 AI 代理撰写与互相审校