跳转到内容

3.3 构造器的组合:从「预制件」到「零件组装」

前两节立起了「类型构造器 → 类型实体」的生成公式。但不少从 Java、Python 或 Go 转过来的开发者打开一个 Rust 项目时,还是会觉得类型签名写法很多:

在其他语言里,表示一串整数通常写 int[] 或 List<Integer>;到了 Rust 里,围绕一串整数或返回值,却会见到 [i32; 4]、&[i32]、Vec<i32>、Option<&i32> 等不同写法。

这一节作为本章的收尾,把背后的原因说清楚:Rust 的类型签名多,不是因为每种写法都是一条孤立的新语法,而是因为其他语言把多项默认决定打包成了「预制件」,而 Rust 把它们拆成了几个正交的「类型构造器零件」,让你按需组合。

回想第 1 章第 1 节的「困难三角」:在 Java 或 Python 里,写一个 ArrayList 或 String 时,语言设计者已经在背后替你做完了四个默认决定:

  1. 数据放哪? 默认放堆上;
  2. 谁管释放? 交给运行时的垃圾回收(GC);
  3. 能不能改? 拿到引用的地方默认都能修改;
  4. 能不能为空? 引用变量默认允许装入 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>>());
}
在 Rust Playground 中运行

运行输出:

运行输出
found: Some(7)
size of [i32; 4]: 16 bytes
size of &[i32]: 16 bytes
size of Option<&i32>: 8 bytes

拿函数 fn first_positive(nums: &[i32]) -> Option<&i32> 里的参数和返回值,演练一次从外向内的拆读:

  1. 拆解参数 nums: &[i32]:
    • 剥第 1 层(最外层是 &):最外层是只读引用构造器 &。这说明 nums 不承担释放责任,且函数内部只能读取不能修改;
    • 剥第 2 层(里面填的是 [i32]):& 里面包着切片类型 [i32],代表一段连续排列的 i32 序列(下一章会看到它为什么占 16 字节)。
  2. 拆解返回值 Option<&i32>:
    • 剥第 1 层(最外层是 Option<...>):Rust 没有隐式的 null,函数可能找不到正数时,用枚举构造器 Option 把结果包起来,要求调用方区分「有值 Some(...)」和「无值 None」(第 6 章详解,包括为什么 Option<&i32> 依然只占 8 字节);
    • 剥第 2 层(里面填的是 &i32):找到正数时,返回的是指向原切片内那个元素的只读引用 &i32,不复制原数组。

语言内置构造器 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);
}
在 Rust Playground 中运行

真正属于语言内置(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> 的,并定义你自己的业务类型。
  1. 从预制件到正交零件:Rust 把数据形状、持有与借用、有无与成败拆成独立的类型构造器,按需组合;
  2. 从外向内剥洋葱:读嵌套类型时先看最外层构造器,它决定当前栈槽位的物理表示与第一层所有权、借用约束;
  3. 内置与标准库分界:&T、[T; N]、[T]、元组是语言内置构造器;Vec<T>、String、Option<T>、Result<T, E> 是标准库用 struct 和 enum 实现的构造器。

本章综合练习:拆读签名与构造器诊断

Section titled “本章综合练习:拆读签名与构造器诊断”

请结合本章三节的「类型实体」与「类型构造器」规则,独立完成下面两道任务:

  1. 剥洋葱与能力裁决:假设你在审查一个函数的参数签名 fn update_cache(items: &mut Vec<String>)。
    • 请从外向内逐层拆出它的三层构造器 / 类型;
    • 变量 items 本身在 64 位栈上占几个字节?函数结束离开作用域时,items 会不会释放那个 Vec?
    • 在函数内部,如果写一行 let backup = items;,原变量 items 还能继续使用吗?为什么(由哪一层构造器决定)?
  2. 按需构造类型签名:假设你要为一个配置加载器设计一个函数签名 fn find_port(...) -> ...:
    • 输入参数:接收一个 User 结构体的只读借用(不接手所有权,也不允许修改);
    • 返回值:如果找到了端口号,返回一个 u16 整数;如果没找到,返回空(不能用魔法数字 0 或 -1 代替)。请用本章见过的构造器写出这个函数的完整签名,并说明如果你不小心把返回类型写成裸的 Option(不带 <...>),编译器会报什么错?
点击核对参考答案
  1. 拆解 items: &mut Vec<String>:
    • 三层结构:最外层是可变引用构造器 &mut;第二层是动态数组构造器 Vec<...>;最内层是元素类型 String。
    • 物理大小与释放责任:因为最外层是 &mut(且 Vec<String> 大小在编译期已知),所以 items 在栈上只占 8 字节(一个内存地址),不承担释放责任,函数返回时不会销毁调用方的 Vec。
    • 赋值行为:执行 let backup = items; 后,原变量 items 不能继续使用(触发移动,再用报 E0382)。因为最外层构造器是 &mut,为了保证可变引用的独占性,&mut 构造器剥夺了生成类型的 Copy 能力。
  2. 函数签名设计:
    • 完整签名应写为:fn find_port(user: &User) -> Option<u16>。
    • 如果把返回类型写成裸的 Option,因为 Option 只是一个尚未填入类型参数 T 的类型构造器,还不是具体的类型实体,编译器会报 error[E0107]: missing generics for enum Option。

进入第 4 章:引用、数组与切片 →