跳转到内容

3.2 类型构造器:新类型是怎样生成的

在展开这一节之前,我们先要把一个最容易被忽视的认知立稳:在 Rust 中,T、&T、&mut T 不是「一个类型加两种借用修饰」,而是三个完全独立的类型。

User、&User、&mut User 是三个独立的类型

Section titled “User、&User、&mut User 是三个独立的类型”

许多刚学完「借用」的读者,或者有 C/C++ 背景的开发者,看到下面这段代码里的 User、&User、&mut User 时,脑子里会产生一种错觉:觉得这里自始至终只有一个类型叫 User,而前面的 & 和 &mut 只不过是临时贴在 User 身上的修饰符(就像 C 语言里的 const 一样)。

这个直觉在 Rust 里是完全错误的。先看一段定义并使用 User 的代码(关于 struct 的基础语法,可随时翻阅基础速查:struct、enum 与 match):

源文件:on-demand-type.rs
// Uncomment the marked line to trigger error[E0308].
struct User {
id: i32,
}
fn main() {
let mut u: User = User { id: 1 };
let r: &User = &u;
println!("{}", r.id);
let m: &mut User = &mut u;
m.id = 2;
// let r2: &mut User = &u;
println!("{}", m.id);
}
在 Rust Playground 中运行

在 Rust 编译器的类型表里,User、&User、&mut User 是三个完全平级、互不隶属的类型实体——它们之间的差异,就像 i32 和 String 一样分明(你甚至可以在脑子里把它们直接读作 User、RefUser、MutRefUser 三个不同的类型名字):

类型符号 栈上物理表示 是否实现 Copy 允许的操作与职责
User 4 字节(直接存放 id: i32) 否(默认未实现,赋值即移动) 持有所有权,负责在离开作用域时释放
&User 8 字节(存放 User 的栈地址) 是(可任意按位复制) 只读借用视图,不承担释放责任
&mut User 8 字节(存放 User 的栈地址) 否(赋值即移动) 独占可变视图,允许修改字段,不承担释放责任

如果取消代码中 // let r2: &mut User = &u; 这一行的注释,试图把 &User 赋给 : &mut User 的变量,编译器报出的不是「借用修饰不对」,而是最直接的类型不匹配(error[E0308]: mismatched types)——因为在编译器眼里,你正在把一个类型的值硬塞给另一个完全不同的类型。

把「T、&T、&mut T 是三个独立类型」认清之后,上一节末尾留下的那个问题就变得无比尖锐了:

回头看 on-demand-type.rs 的前三行,我们在源码里明明只写过 struct User 这一个类型的定义,从头到尾没有手写过任何 RefUser 或 MutRefUser。既然 &User 和 &mut User 是两个完全独立的新类型,它们究竟是谁定义的?又是在什么时候被创建出来的?

答案是:标准库和编译器不可能提前把世界上所有结构体(包括你刚刚取名叫 User 的结构体)的引用类型预定义好。

编译器内置的,是一套用来生成新类型的机制——类型构造器(Type Constructor)。

以 & 和 &mut 为例:

  • & 和 &mut 本身并不是可以直接拿来声明变量的具体类型,它们是两个内置的类型构造器;
  • 如果你的整个程序从头到尾只用了 User,从来没写过 &User,编译器在编译期间就根本不会去生成 &User 这个类型实体;
  • 只有当编译器扫描源码、看到你在某一行写下 &User(或 &u)时,它才会当场调用 & 构造器,把 User 作为输入参数喂进去,在编译期按需合成出 &User 这个全新的类型实体。同理,看到 &mut User 时,再调用 &mut 构造器按需合成出第三个类型实体。

一旦分清了「类型构造器」与「类型实体」,整个 Rust 类型系统的生成逻辑就可以浓缩成一条公式:

类型构造器 + 编译期参数 → 类型实体

一个类型构造器如果还没有填满编译期参数,它还不是具体的类型实体,不能直接作为变量的类型使用。看下面这段代码:

源文件:missing-generics.rs
// Uncomment the marked line to trigger error[E0107].
fn main() {
let v1: Vec<i32> = Vec::new();
let v2: Vec<String> = Vec::new();
// let v3: Vec = Vec::new();
println!("{} {}", v1.len(), v2.len());
}
在 Rust Playground 中运行

Vec<i32> 和 Vec<String> 都能正常编译,它们是把 i32 和 String 填入 Vec 构造器后生成的两个独立类型实体。但一旦取消 // let v3: Vec = Vec::new(); 的注释,试图直接用裸 Vec 当作变量类型,编译器会当场拦截:

编译器诊断摘要:取消 missing-generics.rs 中标记行的注释后
error[E0107]: missing generics for struct `Vec`

在 Rust 中,类型构造器一共可以接收三种不同种类的编译期参数:

最常见的参数就是另一个类型。比如把 i32 或 String 喂给动态数组构造器 Vec<T>,分别生成 Vec<i32> 与 Vec<String>;把 User 喂给引用构造器 &T,生成 &User。

构造器的参数槽里不仅能填类型,还能填编译期已知的常量数字。最典型的例子就是定长数组构造器 [T; N]:

源文件:array-const-param.rs
// Uncomment the marked line to trigger error[E0308].
fn main() {
let a: [i32; 3] = [1, 2, 3];
let b: [i32; 4] = [1, 2, 3, 4];
// let c: [i32; 3] = b;
println!("{} {}", a[0], b[0]);
}
在 Rust Playground 中运行

这里 [i32; 3] 和 [i32; 4] 填入的长度常量不同(一个是 3,一个是 4),生成的就是两个完全不同的类型实体(一个在栈上占 12 字节,一个占 16 字节)。取消 // let c: [i32; 3] = b; 的注释,编译器会直接报类型不匹配(error[E0308]: expected an array with a size of 3, found one with a size of 4)。

3. 生命周期区间参数('a,日常可省略)

Section titled “3. 生命周期区间参数('a,日常可省略)”

回想第一篇第 1 章第 5 节:为什么两个引用同样写着 &String,指向内层短命变量的引用却不能赋给外层变量?

因为在底层,引用构造器除了接收目标类型 T,还会接收一个代表「有效区间」的生命周期参数(记作 'a,完整形式为 &'a T 与 &'a mut T)。指向不同存活区间的引用,填入的区间参数不同,生成的就是两个不同的类型实体。

不过你不用担心每次写引用都要手写 'a——在绝大多数代码中,编译器都能自动推算并补全这个区间参数,因此平时直接省略 'a,简写为 &T 与 &mut T 即可。 这里先点到为止,什么时候才需要显式写出 'a,我们留到第 11 章专门拆解。


在后续章节中,你会陆续遇到各种复合类型语法。只要用「类型构造器」的眼光去看,它们都是同一套机制。这里我们先点到为止,认识最核心的五类构造器:

类型构造器分类 常用写法 吃的编译期参数 生成的具体类型实体示例 核心作用与对应章节
1. 引用构造器 &T
&mut T
类型 T(及通常可省略的生命周期 'a) &i32、&mut String 构造剥离释放责任的共享只读视图或独占可变视图(第 4 章 4.1)
2. 数组与切片构造器 [T; N]
[T]
类型 T + 常量长度 N(或长度留白) [i32; 4]、[u8] 构造连续排列的同类型元素序列(第 4 章 4.2~4.3)
3. 元组构造器 (T1, T2, ...) 零个或多个不同类型 (i32, bool)、() 将多个不同类型打包为固定顺序的复合类型(0 个元素即为单元类型 ())
4. 结构体与枚举构造器 Vec<T>、Option<T>、Result<T, E> 类型或生命周期参数 Vec<u8>、Option<User> 标准库与用户自定义的数据容器与分支模型(第 5~6 章)
5. 智能指针构造器 Box<T>、Rc<T>、Arc<T> 目标类型 T Box<[i32]>、Rc<String> 按需引入堆分配或运行时引用计数(第 9~10 章)

把一个类型 T 放入类型构造器、产出新类型实体时,有一条初学者最容易忽视的规则:

新类型实体的安全约束(比如能不能 Copy、能不能修改),不是从里面的 T 原封不动继承来的,而是由最外层的类型构造器重新制定的。

回看我们在上一节和本节开头见过的两个反差,用这条规则一照就全通了:

  1. 原本不能 Copy 的类型,套上 & 构造器后变成了可 Copy String 和 User 本身没有实现 Copy(赋值即移动);但当你把它们塞进只读引用构造器 &,生成的 &String 和 &User 类型实体剥离了释放责任,它们的约束表被 & 构造器重新写成了允许 Copy(按位复制 8 字节地址,两边都有效)。
  2. 原本可以 Copy 的类型,套上 &mut 构造器后反而被剥夺了 Copy i32 本身是栈上 4 字节整数,天然实现 Copy;但一旦把它塞进可变引用构造器 &mut,生成的 &mut i32 类型实体在赋值时却变成了移动(原变量当场失效,报 E0382)!

这一节我们立住了三个核心判断:

  1. 三个独立类型:T、&T、&mut T 不是一个类型加修饰符,而是三个完全独立、地位平等的类型实体;
  2. 按需合成公式:「类型构造器 + 编译期参数(T / N / 'a)→ 类型实体」,编译器在遇到 &User 或 Vec<i32> 时才按需合成具体类型实体,未填满参数的构造器不能直接作为变量类型(报 E0107);
  3. 能力重写:构造器产出的新类型实体,其物理表示与安全约束由最外层的构造器重新制定。

请务必把「类型构造器 → 类型实体」这一视角牢牢记在心里。它是贯穿整个第二篇的底层主轴——在接下来的章节里,许多在其他书里需要死记硬背的 Rust 难点(比如为什么切片 &[T] 和动态对象 &dyn Trait 会变成 16 字节胖指针、为什么方法接收者要分 self / &self / &mut self、为什么同一个 for 循环会有三种遍历方式),只要放回这两个概念里,都能顺理成章地推导出来。

下一节,我们就来看看:为什么 Rust 不像其他语言那样只给你几个现成的类型,而是鼓励你用多个类型构造器像拼积木一样组合出 Option<&[i32]>、&mut Vec<String> 这样的复合类型?面对多层嵌套的类型签名,又该如何一眼拆读?

继续:构造器的组合 →