跳转到内容

第 7 章 泛型与 Trait 契约

在第二篇中,我们一直在使用标准库预先定义好的类型构造器:往 Vec<T> 里填入 i32 合成 Vec<i32>,往 Option<T> 里填入 String 合成 Option<String>。同时,我们在第 5 章学会了用 impl User 为单个具体类型挂载专属方法。

本章将视野从「单个具体类型」推向「跨类型的通用抽象」:当我们自己定义带 <T> 的泛型类型与泛型函数时,编译器在底层生成了什么代码?当不同的类型需要对外承诺同一组行为时,Rust 如何通过 Trait(契约) 在不引入类继承的前提下完成静态检查与能力扩展?

本章按五步推进:

  1. 泛型与编译期单态化(Monomorphization):从泛型参数 <T> 的四个位置出发,结合「运行时栈上裸数据不含对象信息头」的物理事实,推导为什么通用代码必须按具体类型在编译期拆分为多份独立机器码。
  2. Trait 契约、泛型约束与孤儿规则:解答为什么不能把 Trait 直接当作变量类型、为什么裸 <T> 内部连加法都无法调用,引入 T: Trait 双向门禁,并从防止单态化冲突出发推导孤儿规则(Coherence)。
  3. 关联类型 vs 泛型 Trait:对比 trait From<T> 与 trait Iterator { type Item; },厘清什么时候一个类型需要对多种输入实现同一契约(一对多),什么时候必须唯一锁定输出类型(一对一)。
  4. 转换契约与一揽子实现(From / Into 与 ? 错误归并):拆解一揽子实现(Blanket Implementation)如何让你只实现 From 即自动获得 Into,看清如何用泛型加 Into 替代传统语言的函数重载,并揭晓第 6 章 ? 运算符跨错误类型自动转换的底层机制。
  5. 章末视野:Rust 语言到底有多小?:从操作系统的「机制与策略分离」出发,看清 Rust 如何通过「#[lang] 字符串协议钩子」与「全生态公共约定 Trait」两大阵营把语言内核压缩到极小,将其他语言的内置特性全部下放给库来实现。

读完本章,检验自己的四个问题

Section titled “读完本章,检验自己的四个问题”
  • 既然 Rust 的值在运行时内存里只存裸字节、不带任何对象信息头,一段写成 fn swap<T>(a: T, b: T) 的泛型函数是靠什么机制同时支持 1 字节的 u8 和 24 字节的 String 的?
  • 为什么不能像写 Java 接口那样直接写 fn notify(item: Summary)?为什么编译器禁止你在自己的项目里为标准库的 Vec<i32> 实现标准库的 Display(孤儿规则)?
  • 为什么我们只为自定义类型实现了 From<ParseIntError>,就能在函数里直接对 ParseIntError 使用 ? 运算符,还能自动获得 .into() 方法?
  • 标准库里的 Trait 分为哪两大阵营?为什么编译器要用 #[lang = "add"] 这样的字符串字面量协议去绑定 Add,而不是在编译器代码里直接写死 std::ops::Add?