3.3 构造器的组合:从「预制件」到「零件组装」
前两节立起了「类型构造器 → 类型实体」的生成公式。但不少从 Java、Python 或 Go 转过来的开发者打开一个 Rust 项目时,还是会觉得类型签名写法很多:
在其他语言里,表示一串整数通常写 int[] 或 List<Integer>;到了 Rust 里,围绕一串整数或返回值,却会见到 [i32; 4]、&[i32]、Vec<i32>、Option<&i32> 等不同写法。
这一节作为本章的收尾,把背后的原因说清楚:Rust 的类型签名多,不是因为每种写法都是一条孤立的新语法,而是因为其他语言把多项默认决定打包成了「预制件」,而 Rust 把它们拆成了几个正交的「类型构造器零件」,让你按需组合。
预制件与正交零件
Section titled “预制件与正交零件”回想第 1 章第 1 节的「困难三角」:在 Java 或 Python 里,写一个 ArrayList 或 String 时,语言设计者已经在背后替你做完了四个默认决定:
- 数据放哪? 默认放堆上;
- 谁管释放? 交给运行时的垃圾回收(GC);
- 能不能改? 拿到引用的地方默认都能修改;
- 能不能为空? 引用变量默认允许装入
null。
这种打包好的「预制件」上手省事,但代价是无论当前场景需不需要,运行时都要承担堆对象头、GC 扫描和空指针检查的成本。
Rust 的做法是把这几个决定拆成互不干扰(正交)的构造器维度,用不到的特性不付出代价,需要的能力通过构造器显式组合:
| 决策维度 | 要回答的工程问题 | 对应的类型构造器零件(后续章节逐一展开) |
|---|---|---|
| 1. 数据形状 | 元素怎么排?长度编译期知道吗? | 单个值 T、编译期定长数组 [T; N]、不定长切片 [T] |
| 2. 持有与借用 | 谁负责释放?只读还是可写? | 直接持有 T、只读借用 &T、可变借用 &mut T、可扩容堆容器 Vec<T> |
| 3. 有无与成败 | 结果可能为空吗?操作可能失败吗? | 必定有值 T、可能为空 Option<T>、可能失败 Result<T, E> |
你在 Rust 代码里见到的各种复合类型签名,都是从这几个维度里挑出零件嵌套拼出来的。下面我们只借用其中两三个零件,练习怎么拆读多层嵌套的类型签名,每个零件的具体机制留到第 4~6 章逐一展开。
剥洋葱法:如何从外向内读懂嵌套类型?
Section titled “剥洋葱法:如何从外向内读懂嵌套类型?”遇到像 Option<&[i32]> 或 &mut Vec<String> 这样套了两三层的类型签名时,该按什么顺序读?
根据上一节的结论——最外层的构造器决定当前值的物理表示与第一层约束——读任何嵌套的 Rust 类型,都采用从外向内剥洋葱的顺序。
看一段具体代码:
源文件:peel-onion.rs
use std::mem::size_of;
fn first_positive(nums: &[i32]) -> Option<&i32> { for n in nums { if *n > 0 { return Some(n); } } None}
fn main() { let data: [i32; 4] = [-3, 0, 7, 12]; let slice: &[i32] = &data; let found: Option<&i32> = first_positive(slice);
println!("found: {:?}", found); println!("size of [i32; 4]: {} bytes", size_of::<[i32; 4]>()); println!("size of &[i32]: {} bytes", size_of::<&[i32]>()); println!("size of Option<&i32>: {} bytes", size_of::<Option<&i32>>());}运行输出:
运行输出found: Some(7)size of [i32; 4]: 16 bytessize of &[i32]: 16 bytessize of Option<&i32>: 8 bytes拿函数 fn first_positive(nums: &[i32]) -> Option<&i32> 里的参数和返回值,演练一次从外向内的拆读:
- 拆解参数
nums: &[i32]:- 剥第 1 层(最外层是
&):最外层是只读引用构造器&。这说明nums不承担释放责任,且函数内部只能读取不能修改; - 剥第 2 层(里面填的是
[i32]):&里面包着切片类型[i32],代表一段连续排列的i32序列(下一章会看到它为什么占 16 字节)。
- 剥第 1 层(最外层是
- 拆解返回值
Option<&i32>:- 剥第 1 层(最外层是
Option<...>):Rust 没有隐式的null,函数可能找不到正数时,用枚举构造器Option把结果包起来,要求调用方区分「有值Some(...)」和「无值None」(第 6 章详解,包括为什么Option<&i32>依然只占 8 字节); - 剥第 2 层(里面填的是
&i32):找到正数时,返回的是指向原切片内那个元素的只读引用&i32,不复制原数组。
- 剥第 1 层(最外层是
语言内置构造器 vs 标准库构造器
Section titled “语言内置构造器 vs 标准库构造器”看着上面出现的 &T、[T; N]、Vec<T>、String、Option<T>,初学者常会以为这些名字全都是写死在 Rust 编译器内核里的特殊语法。
其实并非如此:
源文件:builtin-vs-std.rs
fn main() { // 1. Built-in constructors (known directly by the compiler): let arr: [i32; 3] = [1, 2, 3]; let slice_ref: &[i32] = &arr;
// 2. Standard library constructors (ordinary structs/enums in std::prelude): let vec_owned: std::vec::Vec<i32> = vec![1, 2, 3]; let maybe_num: std::option::Option<i32> = std::option::Option::Some(42);
println!("slice len: {}, vec len: {}, maybe: {:?}", slice_ref.len(), vec_owned.len(), maybe_num);}真正属于语言内置(Built-in,由编译器直接定义)的类型构造器只有少数几个基础零件:
- 引用构造器:
&T、&mut T(以及裸指针*const T、*mut T) - 数组与切片构造器:
[T; N]、[T](以及字符串切片str) - 元组构造器:
(T1, T2, ...)与单元类型()
而日常高频使用的 Vec<T>、String、Option<T>、Result<T, E>、Box<T>,都不是语言关键字,而是标准库用第 5、6 章要学的 struct(结构体)和 enum(枚举) 定义出来的库类型(因为标准库默认导入了 std::prelude,所以无需手写 use 即可直接使用,详见基础速查:模块、可见性与 Prelude)。
分清这层边界,第二篇接下来的推进顺序就很清楚了:
- 第 4 章:拆解最核心的内置构造器与连续内存家族——引用(
&T/&mut T)、数组([T; N])、切片([T]/&[T])、动态数组(Vec<T>)与字符串(str/&str/String); - 第 5~6 章:进入自定义类型构造器
struct与enum,看懂标准库是如何造出Option<T>和Result<T, E>的,并定义你自己的业务类型。
小结与本章综合练习
Section titled “小结与本章综合练习”- 从预制件到正交零件:Rust 把数据形状、持有与借用、有无与成败拆成独立的类型构造器,按需组合;
- 从外向内剥洋葱:读嵌套类型时先看最外层构造器,它决定当前栈槽位的物理表示与第一层所有权、借用约束;
- 内置与标准库分界:
&T、[T; N]、[T]、元组是语言内置构造器;Vec<T>、String、Option<T>、Result<T, E>是标准库用struct和enum实现的构造器。
本章综合练习:拆读签名与构造器诊断
Section titled “本章综合练习:拆读签名与构造器诊断”请结合本章三节的「类型实体」与「类型构造器」规则,独立完成下面两道任务:
- 剥洋葱与能力裁决:假设你在审查一个函数的参数签名
fn update_cache(items: &mut Vec<String>)。- 请从外向内逐层拆出它的三层构造器 / 类型;
- 变量
items本身在 64 位栈上占几个字节?函数结束离开作用域时,items会不会释放那个Vec? - 在函数内部,如果写一行
let backup = items;,原变量items还能继续使用吗?为什么(由哪一层构造器决定)?
- 按需构造类型签名:假设你要为一个配置加载器设计一个函数签名
fn find_port(...) -> ...:- 输入参数:接收一个
User结构体的只读借用(不接手所有权,也不允许修改); - 返回值:如果找到了端口号,返回一个
u16整数;如果没找到,返回空(不能用魔法数字0或-1代替)。请用本章见过的构造器写出这个函数的完整签名,并说明如果你不小心把返回类型写成裸的Option(不带<...>),编译器会报什么错?
- 输入参数:接收一个
点击核对参考答案
- 拆解
items: &mut Vec<String>:- 三层结构:最外层是可变引用构造器
&mut;第二层是动态数组构造器Vec<...>;最内层是元素类型String。 - 物理大小与释放责任:因为最外层是
&mut(且Vec<String>大小在编译期已知),所以items在栈上只占 8 字节(一个内存地址),不承担释放责任,函数返回时不会销毁调用方的Vec。 - 赋值行为:执行
let backup = items;后,原变量items不能继续使用(触发移动,再用报E0382)。因为最外层构造器是&mut,为了保证可变引用的独占性,&mut构造器剥夺了生成类型的Copy能力。
- 三层结构:最外层是可变引用构造器
- 函数签名设计:
- 完整签名应写为:
fn find_port(user: &User) -> Option<u16>。 - 如果把返回类型写成裸的
Option,因为Option只是一个尚未填入类型参数T的类型构造器,还不是具体的类型实体,编译器会报error[E0107]: missing generics for enum Option。
- 完整签名应写为: