跳转到内容

第 4 章 引用与数据容器

在上一章中,我们建立了第二篇的核心视角:每一个具体类型都是由类型构造器填入编译期参数后生成的类型实体,同时承载着运行时的物理表示与编译期的安全约束。

但光有理论框架还不够。在日常写 Rust 代码时,最先拦住去路的往往是一连串关于「引用」与「核心数据容器」的实战问题:

  • 为什么 &T 可以随便复制,而同样占 8 字节地址的 &mut T 却不能复制,且同一时刻只能存在一个?
  • 为什么 Rust 要把连续内存序列拆成 [T; N](数组)、[T] / &[T](切片) 与 Vec<T>(动态数组) 三种不同的形态?
  • 为什么别的语言里只有一个 String,Rust 里却同时存在 str、&str、String 与 &String,而且连最常见的按下标取字符 s[0] 都不允许?
  • 为什么往 HashMap<K, V> 里插入键值会让原变量失效,且更新哈希表时普遍使用 .entry(key).or_insert(...)?

这一章我们直接拿上一章的构造器模型,按从简到繁的顺序把这批最高频的基础复合类型逐个拆透:

  • 第一步:引用与可变引用(&T 与 &mut T)——补齐第一章留下的「如何通过引用修改数据」缺口。看清 & 与 &mut 这两个构造器如何在 8 字节瘦指针的相同物理表示上,定出截然相反的复制能力与读写排他规则。
  • 第二步:从定长数组([T; N])到切片胖指针(&[T])——把编译期长度 N 焊进类型实体,换来了栈上零额外头部的 [T; N],但也带来了不同长度数组互不兼容的局限;顺着「擦掉 ; N」的设计思路,推导出动态大小类型 [T] 与 16 字节胖指针 (ptr, len)。
  • 第三步:动态数组(Vec<T>)与连续内存铁三角——当需要在运行时动态增删元素并自己持有所有权时,引出栈上 24 字节控制头 (ptr, len, cap) 操控堆内存的 Vec<T>,从扩容重分配的物理过程推导出禁止悬垂借用与禁止下标移出的安全防线。
  • 第四步:字符串家族(str、&str 与 String)——在 [u8]、&[u8] 与 Vec<u8> 铁三角之上加上一条「合法 UTF-8 编码」约束,彻底厘清四种字符串写法的物理本质、只读字面量的内存归属,以及 UTF-8 变长编码下的安全操作边界。
  • 第五步:键值容器(HashMap<K, V>)与 Entry 模式——从单类型参数序列走向双类型参数构造器 HashMap<K, V>,拆解插入与查询中的所有权交接与借用边界,并看清借用规则如何逼出一次查找原地更新的 Entry 模式。

读完这一章,无论是在自己编写业务代码还是审查 AI 生成的代码时,你都应该能清晰地回答以下三个工程问题:

  1. 物理形态是什么:手里的变量在栈上是 8 字节瘦指针、16 字节胖指针 (ptr, len)、24 字节控制头 (ptr, len, cap),还是直接内联在栈上的连续元素?它指向的数据住在栈、堆,还是只读数据段?
  2. 为什么编译器会拦截:为什么持有容器内部元素引用(如 &v[0] 或 map.get(k))时不能继续 push 或 insert?为什么不能写 let x = v[0]; 移走单个 String?为什么字符串切片切在汉字中间会触发 panic?
  3. 接口签名与写法怎么选:定义函数参数、返回值和结构体字段时,在 [T; N]、Vec<T>、&[T] 以及 String、&str 之间该如何选型?更新 HashMap<K, V> 时为什么优先用 entry(k).or_insert(...)?