跳转至

引用的用法 — 借用的规矩与日常手感

这一章讲两件事: 引用这个「不接管所有权的指针」有哪两种、各自的规矩; 以及写代码时的日常手感——哪些地方要写 & 和 *、哪些地方编译器替你写。 章首引了 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++ 的引用是所指对象的别名。

. 号隐式解引用的完整例子

书里用一个小结构体把自动解引用钉死32:

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} 一次 * 都不用写33。

比较运算也穿透引用:rrx == rry 比的是最终指向的值,不是地址; 真要比地址用 std::ptr::eq。两边类型必须完全对得上(&i32 不能和 &&i32 比)。

4.1 与 C++ 引用的逐句对照

书上有成段的对照,把两种引用的「手感差」讲得最透26:

// 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 的引用是「一种值」——它能被存进变量、传进函数、拿来比较, 给它赋值只是让它指向别处,不惊动它原来指的东西27。

还记得第 1 章 show 函数里我们全程没写 * 吗?那就是 . 号的功劳—— 字段访问和方法调用替你解引用,所以「读字段」感觉不到引用的存在; 只有「读写整个值」(如 *m += 32)才需要显式的 *28。

4.2 共享引用是 Copy,可变不是

一个藏在类型细节里的事实:共享引用 &T 是 Copy 类型—— 复制它,只是多一个只读入口,不产生新的所有权问题;可变引用 &mut T 不是 Copy—— 复制它,排他性就被打破了(两个副本都能写,规矩白立)29。

这一条和第 6 章的 Copy 规则严丝合缝:凡「复制会让语义变质」的类型都不 Copy。

5. 引用永不空,「可能没有」交给 Option

Rust 引用没有空值:没有 NULL/nullptr 的对应物,没有默认初始值, 整数不能转成引用——零变不成引用14。

那怎么表达「可能有一个、可能没有」?Option<&T>。机器层面 None 就是一个空地址, Some(r) 就是那个非零地址——所以 Option<&T> 和 C 的可空指针一样快,但更安全: 类型逼你先查是不是 None 才能用15。 (上一章说过 Option<&T> 和 &T 占一样大的内存——两处说的是同一件事。)

5.1 None 在机器层面就是空地址

「Option<&T> 和 &T 一样快」不是修辞,是布局承诺:

  • Some(r) 存的就是 r 那个非零地址;
  • None 存的就是全零的空地址。

因为 Rust 引用永不为零,编译器拿零当 None 的标记,一个机器字都不用多花。 对照 C 的可空指针:同样的一个字、同样的零值,区别只在于—— Rust 的类型逼你在用之前处理 None,C 允许你解一个空指针然后听天由命30。

5.2 没有「默认引用」

C++ 里 int &r; 编译不过是因为引用必须初始化;Rust 里也一样, 引用没有默认初始值——其实 Rust 里所有类型都没有「未经初始化就能用」的说法。 上一章移动语义留下的「源变量未初始化」状态,和这里的「引用必须有值」,是同一枚硬币的两面: Rust 编译器拒绝一切「值在哪说不清」的程序31。

6. 可以借任何表达式的值

C/C++ 的 & 只能用在特定表达式上;Rust 的 & 可以借任何表达式的值:

let r = &factorial(6);       // 6! = 720 存进一个匿名变量,r 指向它
assert_eq!(r + &1009, 1729); // 算术运算符能看穿一层引用

机制:Rust 造一个匿名变量存表达式的值,让引用指向它。匿名变量活多久取决于引用怎么用: 赋给了 let 变量(或立刻装进结构体/数组),就活到那个变量死;否则只活到当前语句结束16。 听着危险?Rust 保证:只要引用可能被用得太久,编译期就报错,让你把值挪进有名字的变量17。

7. 胖指针:引用不都是一个机器字

普通引用是一个地址。但 Rust 有两种胖指针——两个机器字,地址之外还带一件使用它必需的信息:

  • 切片引用 &[T]:地址 + 元素个数(第 2 章见过);
  • trait 对象引用 &dyn Trait:地址 + 指向该值那套 trait 实现的指针(用来调方法)。

除此之外它们和普通引用一样:不拥有、不许活得比所指长、可以共享或可变的18。

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 吞掉了整张表,于是提示你改签名20。

原书还有一个反向用法值得记住:小型的 Copy 类型直接按值传。整数、布尔这类值, 复制一字节的开销可以忽略,按值传反而省去借用的心智负担——这一档不在「三个选项」的纠结范围内21。

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 的类型引导,实际穿过三层引用才够到那个 Point22。

比较运算同样穿透:rrx == rry 比较的是最终指向的值,不是地址。想真比地址, 用 std::ptr::eq——它把两个引用当地址看23:

let x = 10; let y = 10;
let rx = &x; let ry = &y;
let rrx = &rx; 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:地址 + 元素个数。它指向的元素可能是数组的、向量的、 也可能是别的切片的一部分——胖指针自己带着长度,所以函数拿到它不必再问「这段多长」24;
  • trait 对象引用 &dyn Trait:地址 + 指向该值那套方法实现(虚表)的指针。 同一个 &dyn Write,指向文件时调文件的 write,指向网络流时调网络流的 write—— 多态就靠这第二个机器字25。

除此之外,胖指针和普通引用规矩全同:不拥有、不许活得比所指长、能共享也能可变。 把它想成「引用 + 一份使用说明」就行。

11. 作者的判断与证据

  • 「引用只是地址,规矩才是新东西」——作者明说:底层实现毫无特别, 特别的是规则,而这类规则「研究语言之外你没见过」19。
  • 「可变引用活着时连主人都冻结」是机制事实;「保持共享与修改完全分离对内存安全必不可少」 的论证在下一节(Sharing Versus Mutation),本章只下了结论。
  • 对 C++ 的各处对照(赋值语义、隐式转换)忠实于两语言规范,可信。

12. 边界与局限

  • trait 对象只给了定义,dyn 的用法、虚表(那个「指向实现的指针」)的细节都在未放出的章节;
  • &mut 重借(reborrow)的规则、与模式匹配的组合本章没展开;
  • 「引用即地址」的运行时图景在跨线程共享 Arc 时多了原子性,本章未涉及。

13. 可带走的

  1. 引用 = 借用:不拥有,不许活得比被借的值长;「借来的要还给主人」;
  2. 共享引用 &T:只读,不限个数,Copy;
  3. 可变引用 &mut T:读写,同一时刻唯一,主人同时被冻结;
  4. 按值传参 = 搬家;按引用传参 = 借用;选型直接决定所有权归谁;
  5. 遍历 &集合 产出元素的引用,全程无所有权易手;
  6. &、* 显式写,只有 . 号和比较运算符隐式解引用/自动借;
  7. 给引用赋值是换指向;C++ 引用则不能换目标、赋值即写所指对象;
  8. 引用永不空;「可能没有」用 Option<&T>,和裸空指针一样快;
  9. &任何表达式 都可以,值进匿名变量;用超了编译器会拦;
  10. 两种胖指针:切片引用(地址+长度)、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」)

  1. 出处:「Chapter 4. References」第 13 段(text/14-ch04-chapter-4-references.txt:13,搜「inabilities」)。Mark Miller 是能力安全(capability security)方向的计算机科学家。 ↩

  2. 出处:「Chapter 4. References」第 76–107 段(text/14-ch04-chapter-4-references.txt:76-107,搜「borrow of moved value」与「destroyed」)。 ↩

  3. 出处:「Chapter 4. References」第 117–123 段(text/14-ch04-chapter-4-references.txt:117-123,搜「borrowed it」)。 ↩

  4. 出处:「Chapter 4. References」第 152 段(text/14-ch04-chapter-4-references.txt:152,搜「by value」)。 ↩

  5. 出处:「Chapter 4. References」第 132–136 段(text/14-ch04-chapter-4-references.txt:132-136,搜「&Vec<String>」)。 ↩

  6. 出处:「Chapter 4. References」第 138–150 段(text/14-ch04-chapter-4-references.txt:138-150,搜「sort_works」)。 ↩

  7. 出处:「Chapter 4. References」第 111 段(text/14-ch04-chapter-4-references.txt:111,搜「as many shared references」)。 ↩

  8. 出处:「Chapter 4. References」第 113 段(text/14-ch04-chapter-4-references.txt:113,搜「may not have any other references」)。 ↩

  9. 出处:「Chapter 4. References」第 115 段(text/14-ch04-chapter-4-references.txt:115,搜「locked down」)。 ↩

  10. 出处:「Chapter 4. References」第 186–196 段(text/14-ch04-chapter-4-references.txt:186-196,搜「implicitly dereferences」)。 ↩

  11. 出处:「Chapter 4. References」第 198–202 段(text/14-ch04-chapter-4-references.txt:198-202,搜「implicitly borrows」)。 ↩

  12. 出处:「Chapter 4. References」第 204 段(text/14-ch04-chapter-4-references.txt:204,搜「In a nutshell」)。 ↩

  13. 出处:「Chapter 4. References」第 222 段(text/14-ch04-chapter-4-references.txt:222,搜「stores the value in its referent」)。 ↩

  14. 出处:「Chapter 4. References」第 272 段(text/14-ch04-chapter-4-references.txt:272,搜「never null」)。 ↩

  15. 出处:「Chapter 4. References」第 274 段(text/14-ch04-chapter-4-references.txt:274,搜「just as efficient」)。 ↩

  16. 出处:「Chapter 4. References」第 287–291 段(text/14-ch04-chapter-4-references.txt:287-291,搜「anonymous variable」)。 ↩

  17. 出处:「Chapter 4. References」第 293 段(text/14-ch04-chapter-4-references.txt:293,搜「dangling reference」)。 ↩

  18. 出处:「Chapter 4. References」第 297–303 段(text/14-ch04-chapter-4-references.txt:297-303,搜「fat pointers」)。 ↩

  19. 出处:「Chapter 4. References」第 21 段(text/14-ch04-chapter-4-references.txt:21,搜「nothing special」)。 ↩

  20. 出处:「Chapter 4. References」第 95–103 段(text/14-ch04-chapter-4-references.txt:95-103,搜「consider changing this parameter type」)。报错信息自带的建议:把参数类型改成借用;「如果不需要拥有这个值的话」。 ↩

  21. 出处:「Chapter 3. Ownership and Moves」第 456 段(text/13-ch03-chapter-3-ownership-and-moves.txt:456,搜「more flexible」)。原文:Copy 类型更灵活,因为赋值这类操作不会把原来的值弄成未初始化。 ↩

  22. 出处:「Chapter 4. References」第 226–242 段(text/14-ch04-chapter-4-references.txt:226-242,搜「traverses three references」)。 ↩

  23. 出处:「Chapter 4. References」第 246–263 段(text/14-ch04-chapter-4-references.txt:246-263,搜「compares them as addresses」)。 ↩

  24. 出处:「Chapter 4. References」第 297–299 段(text/14-ch04-chapter-4-references.txt:297-299,搜「starting address」)。 ↩

  25. 出处:「Chapter 4. References」第 301 段(text/14-ch04-chapter-4-references.txt:301,搜「implementation appropriate」)。 ↩

  26. 出处:「Chapter 4. References」第 162–168 段(text/14-ch04-chapter-4-references.txt:162-168,搜「implicitly」)。 ↩

  27. 出处:「Chapter 4. References」第 206–222 段(text/14-ch04-chapter-4-references.txt:206-222,搜「point somewhere new」)。 ↩

  28. 出处:「Chapter 4. References」第 184–196 段(text/14-ch04-chapter-4-references.txt:184-196,搜「println!」)。 ↩

  29. 出处:「Chapter 4. References」第 111–113 段(text/14-ch04-chapter-4-references.txt:111-113,搜「Shared references are Copy」)。 ↩

  30. 出处:「Chapter 4. References」第 274 段(text/14-ch04-chapter-4-references.txt:274,搜「just as efficient」)。 ↩

  31. 出处:「Chapter 4. References」第 272 段(text/14-ch04-chapter-4-references.txt:272,搜「no default initial value」)。 ↩

  32. 出处:「Chapter 4. References」第 188–196 段(text/14-ch04-chapter-4-references.txt:188-196,搜「Anime」)。 ↩

  33. 出处:「Chapter 4. References」第 196 段(text/14-ch04-chapter-4-references.txt:196,搜「implicit dereference」)。 ↩