引用的用法 — 借用的规矩与日常手感
引用的用法 — 借用的规矩与日常手感
这一章讲两件事: 引用这个「不接管所有权的指针」有哪两种、各自的规矩; 以及写代码时的日常手感——哪些地方要写
&和*、哪些地方编译器替你写。 章首引了 Mark Miller 的一句话:「库不能提供新的不能(libraries cannot provide new inabilities)。」1 本章的主题正相反:语言本身要提供「不能」——不能悬垂、不能空、不能边共享边改。
1. 顶层全景:借用的两条规矩
引用 = 借用:不接管所有权,只是临时指向,必须还(不能活得比被借的值长)
│
├─ 共享引用 &T:只读;想开几个开几个;本身是 Copy
└─ 可变引用 &mut T:读写;同一时刻只能存在一个,连主人都被冻结
│
▼
例外只有「同时」:同一时刻,一个值要么被很多人只读地共享,要么被一个人独占可写图说:这套规矩是下一章全部安全论证的前提,本章先把规矩本身讲清。
2. 为什么需要引用:show 函数吞掉了整张表
书里从一个真实痛点开始。假设有一张表:HashMap<String, Vec<String>>(艺术家名 → 作品列表)。
打印函数写成 fn show(table: Table)——按值传参,整张表被移动进去,打完就销毁。
再想用?编译器报错:borrow of moved value2。
编译器同时给出两条出路:clone(复制一份,可能很贵)或借用。正解是借用:
fn show(table: &Table) { // 只改了这个类型:&Table
for (artist, works) in table {
println!("works by {artist}:");
for work in works {
println!(" {work}");
}
}
}
show(&table); // 调用处加 &改动只有两处,但语义全变了:按引用传参——table 仍是整棵结构的主人, show 只是借来看一眼3。按值(by value)搬所有权、按引用(by reference)只借, 这个区分很多语言都有,但在 Rust 里它直接写着所有权归谁,所以格外要紧4。
遍历引用也有讲究:迭代 &HashMap 产出的是每个键值的引用(&String、&Vec<String>),
内层循环再迭代 &Vec<String> 产出 &String——全程没有任何所有权易手5。
想改内容(比如排序),就要借可变的:fn sort_works(table: &mut Table) 配 sort_works(&mut table)6。
3. 两种引用的规矩
共享引用 &T:&e 产生;只读,不许改;同时想要几个要几个;类型是 Copy——
复制一个共享引用不产生新的所有权问题,因为它本来就不拥有7。
可变引用 &mut T:&mut e 产生;能读能改;同一时刻不许有任何其他引用
(共享的、可变的都不行);不是 Copy8。
这两条合起来就是上一章那句铁律的执行细则:同一时刻,一个值要么被共享只读,要么被独占可写。 而且规矩管的不只是引用:共享引用活着的时候,连主人都不能改这个值——值被整体冻结; 可变引用活着的时候,主人也碰不得,直到可变引用消失9。
4. 日常手感:&、* 什么时候写、什么时候不写
创建与解引用都是显式的(对照 C++ 的隐式转换):
let x = 10;
let r = &x; // 显式借
assert_eq!(*r, 10); // 显式解引用
let m = &mut y;
*m += 32; // 通过可变引用写但 . 号会帮你解: anime_ref.name 等价于 (*anime_ref).name——
编译器看类型需要几层解就解几层,引用的引用也一路穿到底(rrr.y 穿过三层引用)10。
. 还会自动借:v.sort() 自动变成 (&mut v).sort()11。
结论:C++ 在「任何地方隐式转换」,Rust 只特许 . 一个操作符隐式12。
给引用赋值 = 让它指向别处,这和 C++ 截然不同:
let mut r = &x;
if b { r = &y; } // 现在 r 指向 y——改动的是指针本身C++ 里 r = y 是「把 y 的值存进 r 所指的对象」;C++ 引用一旦初始化就再也不能换目标13。
Rust 的引用是一个可以换指向的变量,C++ 的引用是所指对象的别名。
. 号隐式解引用的完整例子
书里用一个小结构体把自动解引用钉死14:
struct Anime { name: &'static str, bechdel_pass: bool }
let aria = Anime { name: "Aria: The Animation", bechdel_pass: true };
let anime_ref = &aria;
assert_eq!(anime_ref.name, "Aria: The Animation");
assert_eq!((*anime_ref).name, "Aria: The Animation"); // 与上一行完全等价anime_ref.name 能编译,是因为 . 号看左边是引用、自动解一层。
println! 宏展开后的代码也走 . 号,所以第 1 章 show 函数里满屏的
{artist}、{work} 一次 * 都不用写15。
比较运算也穿透引用:rrx == rry 比的是最终指向的值,不是地址;
真要比地址用 std::ptr::eq。两边类型必须完全对得上(&i32 不能和 &&i32 比)。
4.1 与 C++ 引用的逐句对照
书上有成段的对照,把两种引用的「手感差」讲得最透16:
// C++:引用是隐式创建、隐式解引用的
int x = 10;
int &r = x; // 初始化就隐式成了引用
assert(r == 10); // 用 r 的地方自动解引用
r = 20; // 给 r 赋值 = 写 x;r 自己永远指 x// Rust:一切都是显式的
let x = 10;
let r = &x; // 显式创建
assert_eq!(*r, 10); // 显式解引用
*r = 20; // 想写所指的对象,先 *r(在 r 是可变引用的前提下)差异不止写法。C++ 的引用是「变量的别名」——一旦绑定,终身不换; Rust 的引用是「一种值」——它能被存进变量、传进函数、拿来比较, 给它赋值只是让它指向别处,不惊动它原来指的东西17。
还记得第 1 章 show 函数里我们全程没写 * 吗?那就是 . 号的功劳——
字段访问和方法调用替你解引用,所以「读字段」感觉不到引用的存在;
只有「读写整个值」(如 *m += 32)才需要显式的 *18。
4.2 共享引用是 Copy,可变不是
一个藏在类型细节里的事实:共享引用 &T 是 Copy 类型——
复制它,只是多一个只读入口,不产生新的所有权问题;可变引用 &mut T 不是 Copy——
复制它,排他性就被打破了(两个副本都能写,规矩白立)19。
这一条和第 6 章的 Copy 规则严丝合缝:凡「复制会让语义变质」的类型都不 Copy。
5. 引用永不空,「可能没有」交给 Option
Rust 引用没有空值:没有 NULL/nullptr 的对应物,没有默认初始值, 整数不能转成引用——零变不成引用20。
那怎么表达「可能有一个、可能没有」?Option<&T>。机器层面 None 就是一个空地址,
Some(r) 就是那个非零地址——所以 Option<&T> 和 C 的可空指针一样快,但更安全:
类型逼你先查是不是 None 才能用21。
(上一章说过 Option<&T> 和 &T 占一样大的内存——两处说的是同一件事。)
5.1 None 在机器层面就是空地址
「Option<&T> 和 &T 一样快」不是修辞,是布局承诺:
Some(r)存的就是 r 那个非零地址;None存的就是全零的空地址。
因为 Rust 引用永不为零,编译器拿零当 None 的标记,一个机器字都不用多花。
对照 C 的可空指针:同样的一个字、同样的零值,区别只在于——
Rust 的类型逼你在用之前处理 None,C 允许你解一个空指针然后听天由命22。
5.2 没有「默认引用」
C++ 里 int &r; 编译不过是因为引用必须初始化;Rust 里也一样,
引用没有默认初始值——其实 Rust 里所有类型都没有「未经初始化就能用」的说法。
上一章移动语义留下的「源变量未初始化」状态,和这里的「引用必须有值」,是同一枚硬币的两面:
Rust 编译器拒绝一切「值在哪说不清」的程序23。
6. 可以借任何表达式的值
C/C++ 的 & 只能用在特定表达式上;Rust 的 & 可以借任何表达式的值:
let r = &factorial(6); // 6! = 720 存进一个匿名变量,r 指向它
assert_eq!(r + &1009, 1729); // 算术运算符能看穿一层引用机制:Rust 造一个匿名变量存表达式的值,让引用指向它。匿名变量活多久取决于引用怎么用: 赋给了 let 变量(或立刻装进结构体/数组),就活到那个变量死;否则只活到当前语句结束24。 听着危险?Rust 保证:只要引用可能被用得太久,编译期就报错,让你把值挪进有名字的变量25。
7. 胖指针:引用不都是一个机器字
普通引用是一个地址。但 Rust 有两种胖指针——两个机器字,地址之外还带一件使用它必需的信息:
- 切片引用
&[T]:地址 + 元素个数(第 2 章见过); - trait 对象引用
&dyn Trait:地址 + 指向该值那套 trait 实现的指针(用来调方法)。
除此之外它们和普通引用一样:不拥有、不许活得比所指长、可以共享或可变的26。
8. 函数参数怎么选:按值、按共享引用、还是按可变引用
一章之内,同一个表就换了三种收法,正好凑成一份决策样本:
| 收法 | 例子 | 什么时候用 |
|---|---|---|
按值 fn show(table: Table) | 把表搬进函数 | 函数要接管这个值、甚至消耗它(比如所有权要转手、要存进结构体) |
共享引用 fn show(table: &Table) | 看一眼就走 | 只读——绝大多数「传进去看看」的函数 |
可变引用 fn sort_works(table: &mut Table) | 改完还你 | 要修改调用方的值,但不接管 |
选择的依据恰好就是上一章的移动语义:按值 = 所有权过户;按引用 = 借用,完璧归赵。
大多数「看看而已」的函数都该用 &T——这也解释了第 1 章那个报错信息为什么主动建议
「consider changing this parameter type … to borrow instead」:编译器从所有权的角度
看出 show 吞掉了整张表,于是提示你改签名27。
原书还有一个反向用法值得记住:小型的 Copy 类型直接按值传。整数、布尔这类值, 复制一字节的开销可以忽略,按值传反而省去借用的心智负担——这一档不在「三个选项」的纠结范围内28。
9. 引用的引用、比较的穿透与地址比较
Rust 允许引用叠引用,而且取字段时一路穿到底:
struct Point { x: i32, y: i32 }
let point = Point { x: 1000, y: 729 };
let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr;
assert_eq!(rrr.y, 729); // . 号连穿三层引用.y 由 rrr 的类型引导,实际穿过三层引用才够到那个 Point29。
比较运算同样穿透:rrx == rry 比较的是最终指向的值,不是地址。想真比地址,
用 std::ptr::eq——它把两个引用当地址看30:
let x = 10; let y = 10;
let rx = &x; let ry = &y;
let rrx = ℞ let rry = &ry;
assert!(rrx <= rry); // 穿透后比的是 10 和 10
assert!(rrx == rry);
assert!(rx == ry); // 所指的两个 10 相等
assert!(!std::ptr::eq(rx, ry)); // 但占着两个不同的地址还有一个类型纪律:比较的双方必须完全同类型,引用层数也算:
assert!(rx == rrx); // 错误:&i32 不能和 &&i32 比
assert!(rx == *rrx); // 剥一层就可以10. 胖指针的内存长相
普通引用是一个机器字(地址)。两种胖指针各多带一个机器字:
- 切片引用
&[T]/&str:地址 + 元素个数。它指向的元素可能是数组的、向量的、 也可能是别的切片的一部分——胖指针自己带着长度,所以函数拿到它不必再问「这段多长」31; - trait 对象引用
&dyn Trait:地址 + 指向该值那套方法实现(虚表)的指针。 同一个&dyn Write,指向文件时调文件的 write,指向网络流时调网络流的 write—— 多态就靠这第二个机器字32。
除此之外,胖指针和普通引用规矩全同:不拥有、不许活得比所指长、能共享也能可变。 把它想成「引用 + 一份使用说明」就行。
11. 作者的判断与证据
- 「引用只是地址,规矩才是新东西」——作者明说:底层实现毫无特别, 特别的是规则,而这类规则「研究语言之外你没见过」33。
- **「可变引用活着时连主人都冻结」**是机制事实;「保持共享与修改完全分离对内存安全必不可少」 的论证在下一节(Sharing Versus Mutation),本章只下了结论。
- 对 C++ 的各处对照(赋值语义、隐式转换)忠实于两语言规范,可信。
12. 边界与局限
- trait 对象只给了定义,
dyn的用法、虚表(那个「指向实现的指针」)的细节都在未放出的章节; &mut重借(reborrow)的规则、与模式匹配的组合本章没展开;- 「引用即地址」的运行时图景在跨线程共享 Arc 时多了原子性,本章未涉及。
13. 可带走的
- 引用 = 借用:不拥有,不许活得比被借的值长;「借来的要还给主人」;
- 共享引用
&T:只读,不限个数,Copy; - 可变引用
&mut T:读写,同一时刻唯一,主人同时被冻结; - 按值传参 = 搬家;按引用传参 = 借用;选型直接决定所有权归谁;
- 遍历
&集合产出元素的引用,全程无所有权易手; &、*显式写,只有.号和比较运算符隐式解引用/自动借;- 给引用赋值是换指向;C++ 引用则不能换目标、赋值即写所指对象;
- 引用永不空;「可能没有」用
Option<&T>,和裸空指针一样快; &任何表达式都可以,值进匿名变量;用超了编译器会拦;- 两种胖指针:切片引用(地址+长度)、trait 对象(地址+实现指针)。
14. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 章题词与借用的定义 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:13(搜「inabilities」) · text/14-ch04-chapter-4-references.txt:19(搜「borrowing the value」) |
| show 吞表问题 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:76(搜「moved」) · text/14-ch04-chapter-4-references.txt:107(搜「destroyed」) |
| 两种引用的规矩 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:111(搜「as many shared references」) · text/14-ch04-chapter-4-references.txt:113(搜「may not have any other references」) |
| 主人也被冻结 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:115(搜「locked down」) |
| 按值/按引用 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:152(搜「by value」) |
| 显式 & 与 * | Chapter 4. References | text/14-ch04-chapter-4-references.txt:170(搜「explicitly」) |
| . 自动解引用与自动借 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:186(搜「implicitly dereferences」) · text/14-ch04-chapter-4-references.txt:201(搜「implicitly borrows」) |
| 给引用赋值=换指向 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:222(搜「stores the value in its referent」) |
| 比较穿透引用 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:246(搜「see through」) |
| 引用永不空 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:272(搜「never null」) |
| Option<&T> 一样快 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:274(搜「just as efficient」) |
| 借任意表达式、匿名变量 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:287(搜「anonymous variable」) |
| 两种胖指针 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:297(搜「fat pointers」) |
| 参数选择的编译器建议 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:95(搜「consider changing this parameter type」) |
| 引用叠引用与比较穿透 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:236(搜「rrr.y」) · text/14-ch04-chapter-4-references.txt:260(搜「compares them as addresses」) |
| C++ 引用逐句对照 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:162(搜「implicitly」) |
| 引用赋值换指向 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:208(搜「point somewhere new」) |
| None 的机器表示 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:274(搜「null pointer」) |
| Anime 例子与自动解引用 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:188(搜「Anime」) |
| trait 对象的第二个机器字 | Chapter 4. References | text/14-ch04-chapter-4-references.txt:301(搜「implementation appropriate」) |
Footnotes
-
出处:「Chapter 4. References」第 13 段(
text/14-ch04-chapter-4-references.txt:13,搜「inabilities」)。Mark Miller 是能力安全(capability security)方向的计算机科学家。 ↩ -
出处:「Chapter 4. References」第 76–107 段(
text/14-ch04-chapter-4-references.txt:76-107,搜「borrow of moved value」与「destroyed」)。 ↩ -
出处:「Chapter 4. References」第 117–123 段(
text/14-ch04-chapter-4-references.txt:117-123,搜「borrowed it」)。 ↩ -
出处:「Chapter 4. References」第 152 段(
text/14-ch04-chapter-4-references.txt:152,搜「by value」)。 ↩ -
出处:「Chapter 4. References」第 132–136 段(
text/14-ch04-chapter-4-references.txt:132-136,搜「&Vec<String>」)。 ↩ -
出处:「Chapter 4. References」第 138–150 段(
text/14-ch04-chapter-4-references.txt:138-150,搜「sort_works」)。 ↩ -
出处:「Chapter 4. References」第 111 段(
text/14-ch04-chapter-4-references.txt:111,搜「as many shared references」)。 ↩ -
出处:「Chapter 4. References」第 113 段(
text/14-ch04-chapter-4-references.txt:113,搜「may not have any other references」)。 ↩ -
出处:「Chapter 4. References」第 115 段(
text/14-ch04-chapter-4-references.txt:115,搜「locked down」)。 ↩ -
出处:「Chapter 4. References」第 186–196 段(
text/14-ch04-chapter-4-references.txt:186-196,搜「implicitly dereferences」)。 ↩ -
出处:「Chapter 4. References」第 198–202 段(
text/14-ch04-chapter-4-references.txt:198-202,搜「implicitly borrows」)。 ↩ -
出处:「Chapter 4. References」第 204 段(
text/14-ch04-chapter-4-references.txt:204,搜「In a nutshell」)。 ↩ -
出处:「Chapter 4. References」第 222 段(
text/14-ch04-chapter-4-references.txt:222,搜「stores the value in its referent」)。 ↩ -
出处:「Chapter 4. References」第 188–196 段(
text/14-ch04-chapter-4-references.txt:188-196,搜「Anime」)。 ↩ -
出处:「Chapter 4. References」第 196 段(
text/14-ch04-chapter-4-references.txt:196,搜「implicit dereference」)。 ↩ -
出处:「Chapter 4. References」第 162–168 段(
text/14-ch04-chapter-4-references.txt:162-168,搜「implicitly」)。 ↩ -
出处:「Chapter 4. References」第 206–222 段(
text/14-ch04-chapter-4-references.txt:206-222,搜「point somewhere new」)。 ↩ -
出处:「Chapter 4. References」第 184–196 段(
text/14-ch04-chapter-4-references.txt:184-196,搜「println!」)。 ↩ -
出处:「Chapter 4. References」第 111–113 段(
text/14-ch04-chapter-4-references.txt:111-113,搜「Shared references are Copy」)。 ↩ -
出处:「Chapter 4. References」第 272 段(
text/14-ch04-chapter-4-references.txt:272,搜「never null」)。 ↩ -
出处:「Chapter 4. References」第 274 段(
text/14-ch04-chapter-4-references.txt:274,搜「just as efficient」)。 ↩ -
出处:「Chapter 4. References」第 274 段(
text/14-ch04-chapter-4-references.txt:274,搜「just as efficient」)。 ↩ -
出处:「Chapter 4. References」第 272 段(
text/14-ch04-chapter-4-references.txt:272,搜「no default initial value」)。 ↩ -
出处:「Chapter 4. References」第 287–291 段(
text/14-ch04-chapter-4-references.txt:287-291,搜「anonymous variable」)。 ↩ -
出处:「Chapter 4. References」第 293 段(
text/14-ch04-chapter-4-references.txt:293,搜「dangling reference」)。 ↩ -
出处:「Chapter 4. References」第 297–303 段(
text/14-ch04-chapter-4-references.txt:297-303,搜「fat pointers」)。 ↩ -
出处:「Chapter 4. References」第 95–103 段(
text/14-ch04-chapter-4-references.txt:95-103,搜「consider changing this parameter type」)。报错信息自带的建议:把参数类型改成借用;「如果不需要拥有这个值的话」。 ↩ -
出处:「Chapter 3. Ownership and Moves」第 456 段(
text/13-ch03-chapter-3-ownership-and-moves.txt:456,搜「more flexible」)。原文:Copy 类型更灵活,因为赋值这类操作不会把原来的值弄成未初始化。 ↩ -
出处:「Chapter 4. References」第 226–242 段(
text/14-ch04-chapter-4-references.txt:226-242,搜「traverses three references」)。 ↩ -
出处:「Chapter 4. References」第 246–263 段(
text/14-ch04-chapter-4-references.txt:246-263,搜「compares them as addresses」)。 ↩ -
出处:「Chapter 4. References」第 297–299 段(
text/14-ch04-chapter-4-references.txt:297-299,搜「starting address」)。 ↩ -
出处:「Chapter 4. References」第 301 段(
text/14-ch04-chapter-4-references.txt:301,搜「implementation appropriate」)。 ↩ -
出处:「Chapter 4. References」第 21 段(
text/14-ch04-chapter-4-references.txt:21,搜「nothing special」)。 ↩