跳转至

引用规则的实战判例 — 把「多读或单写」用到具体代码上

这一章讲一件事: 上一章给了机制(生命周期怎么查),这一章给手感—— 一条条具体代码,先自己判「能不能编译」,再看 Rust 怎么判。判完这一串, 你对两条规则(共享即只读、可变即排他)的直觉就立住了。

1. 顶层全景:两组判例,一个主线

  判例组一:最小例子(书上「把原则削到最简」的那组)
    同一时刻能有什么引用?——四个场景逐个判
  判例组二:真实 bug 现场
    向量扩容悬垂 / C++ File 自赋值 / HashMap 借用 / sea of objects
      │
      ▼
  主线例子:x 周围的一组借用,从全合法到全非法排成一个光谱

图说:左边是「规矩的字面」,右边是「规矩拦下来的真 bug」,中间靠一组渐变例子衔接。

2. 主线例子:围绕一个 x,四种借法排成光谱

书上把两条规则削到一组最小例子(原书 §"Paring these principles down")1。 取一个变量 x,看哪些引用组合合法:

场景 A:多个共享引用 + 想改 x——不合法。

let mut x = 10;
let r1 = &x;
let r2 = &x;      // 可以:共享引用要几个给几个
x += 10;          // 错误:不能给 x 赋值,它被借走了
let m = &mut x;   // 错误:不能借可变的,已有共享借用
println!("{r1}, {r2}, {m}");

关键点在最后一行:r1、r2、m 在下面被用,所以它们的寿命必须至少延伸到那里—— 冻结也就持续到那里。如果删掉 println,借用提前结束,x 的赋值就合法了2。

场景 B:两个可变引用——不合法。

let mut y = 20;
let m1 = &mut y;
let m2 = &mut y;  // 错误:只能借一次可变的
let z = y;        // 错误:可变的借着,主人也碰不得
println!("{m1}, {m2}, {z}");

场景 C:从共享引用再借共享——合法;再借可变——不合法。

let mut w = (107, 109);
let r = &w;
let r0 = &r.0;    // 可以:共享里再借共享
let m1 = &mut r.1;// 错误:不能隔着共享借可变
println!("{r0}");

场景 D:从可变引用再借——两种都可以。

let mut v = (136, 139);
let m = &mut v;
let m0 = &mut m.0;   // 可以:可变里再借可变
*m0 = 137;
let r1 = &m.1;       // 可以:可变里借共享,且与 m0 不重叠
v.1;                 // 错误:走别的路径仍被禁止
println!("{r1}");

这组例子的读法:「再借」永远顺着原来的引用走——从共享只能再借共享,从可变两种都能借; 但无论怎样,绕开当前引用、走所有权路径直接碰值,一律禁止3。

3. 判例:往正在指着的向量里塞元素

这是书上最完整的一个 bug 现场,值得走全程。工具函数:

fn extend(vec: &mut Vec<f64>, slice: &[f64]) {
    for elt in slice {
        vec.push(*elt);
    }
}

用法一路正常:

let mut wave = Vec::new();
let head = vec![0.0, 1.0];
let tail = [0.0, -1.0];
extend(&mut wave, &head);   // 用另一个向量扩展
extend(&mut wave, &tail);   // 用数组扩展
assert_eq!(wave, vec![0.0, 1.0, 0.0, -1.0]);

现在想加一个周期——把 wave 追加给自己:extend(&mut wave, &wave);。 casual 看毫无问题。推演一下灾难:

  1. 假设 wave 容量 4、已装 4 个,&mut wave 借出,&wave 指向它的元素;
  2. extend 往里 push 第 5 个,缓冲满了,分配一块 8 个的新缓冲,旧 4 个拷过去,旧的释放;
  3. 但 slice 还指着旧缓冲——已经还给了系统。继续迭代 = 读已释放的内存4。

这类 bug 的阴险处在最后:测试时容量总是够,缓冲永不重分配,问题可能永远不露头; 一上生产、数据一大就爆。C++ 的 vector 规范同样警告「重分配使指向元素的引用、指针、 迭代器全部失效」——但 C++ 只能在你踩中时让你崩,Rust 在编译时就拒: 「cannot borrow wave as immutable because it is also borrowed as mutable」5。

Rust 的判词可以读成两种等价说法,对应两条规则:可变借用要求排他(共享切片是第二条路径, 违反规则二);或:共享借用冻结整条路径(不能再借可变,违反规则一)。同一个错,两条规则都接得住6。

4. 判例:C 的 const 指针为什么不够严

C 程序员会觉得「共享引用不就是 const int *p 吗」——书上专门一段拆这个误解7。

C 的代码:

int x = 42;
const int *p = &x;
assert(*p == 42);
x++;                      // 直接改 x,p 不是唯一入口
assert(*p == 43);         // 「常量」所指的那个值,变了

const int *p 只禁止通过 p 自己改值:(*p)++ 不行,但 x 不是 const, 从旁边照样改。C 的 const 是「这一个入口只读」,不是「这个值只读」8。

Rust 的同形代码:

let mut x = 42;
let p = &x;
assert_eq!(*p, 42);
x += 1;    // 错误:不能给 x 赋值,它被借走了

共享引用活着,x 从任何路径都不能改。要做到这一点,编译器必须掌握通向这个值的 所有路径并确认它们此刻都只读——C 的指针太自由,查不了;Rust 的引用绑在寿命上, 查得了。这也是「引用永不空、创建显式、转换显式」这些麻烦规定的回报9。

5. 判例:C++ File 自赋值的完整现场

书里最完整的「同一类 bug」故事,三步走完。

第一步,C++ 的翻车。 一个管文件描述符的类,赋值运算符:

File& operator=(const File &rhs) {
    close(descriptor);          // 先关掉自己手里的
    descriptor = dup(rhs.descriptor);  // 再复制对方的
    return *this;
}

平时正确。但 f = f;(自己赋自己):rhs 和 *this 是同一个对象—— close 关掉的,正是马上要拿去 dup 的那个描述符。资源被亲手销毁10。

第二步,Rust 的同形代码。 直接照搬:

fn clone_from(this: &mut File, rhs: &File) {
    close(this.descriptor);
    this.descriptor = dup(rhs.descriptor);
}
clone_from(&mut f, &f);   // ← 自赋值同形调用

第三步,Rust 的判决。 拒绝编译:cannot borrow f as immutable because it is also borrowed as mutable。「可变访问即排他」——既然 &mut f 活着, 就不存在第二条通向 f 的路径,&f 借不出来11。

作者的点评值得记住:「没处理自赋值」和「用了失效迭代器」看起来是两个不相干的 C++ bug, 其实是同一个底层错误的两种形态——代码以为自己一边改一个值、一边读另一个值, 实际两个是同一个值。C 里 memcpy/strcpy 的源和目标一旦重叠,同款。 要求可变访问排他,Rust 把这一整类日常错误整体挡在门外12。

6. 判例:借用 HashMap 的一个键

规则的应用不限于向量。从 HashMap 借一个键的共享引用:

let key_ref = &map["some key"];
// 在 key_ref 还活着时:
map.insert("another", value);   // 错误:不能借可变的

原因和快排切片同款:insert 可能触发扩容/重构,key_ref 指向的内存可能搬家。 规则不区分容器品种——只要共享引用活着,整条所有权路径(这里是整个 map)冻结; 等 key_ref 最后一次用完,map 自然解冻13。

这一条写起来紧,但原书给了它正当性:让容器支持「无限制的边遍历边修改」很难, 而且常常逼掉更简单高效的实现。Java 的 Hashtable 直接抛异常不管, C++ 的 map 为了保证「插入不影响既有指针」承诺,排除了 Rust BTreeMap 那种 每个节点存多条目的缓存友好设计。宽松不是免费午餐14。

7. 并发判例:数据竞争按构造消失

两条规则最大的检验场是并发。把「可变即排他」放到多线程下读:

数据竞争的全部构成要件是——某个值既可变、又被多个线程共享,两边不同步地读写, 结果取决于时序。而引用的规矩恰恰是:任何时刻,一个值要么只读共享、要么独占可变, 「既可变又被共享」这个状态在类型层面就不存在。所以不靠任何锁、任何运行时检查, 一个不用 unsafe 的 Rust 并发程序,按构造( by construction)就是无数据竞争的15。

这就是第 1 章那个大承诺的落地处:「如果你的程序编译过了,它就是无数据竞争的」。 代价和收益对得很齐:规矩在单线程里显得紧,到了多线程里,紧就变成了护身符。 书上甚至说,写并发代码时共享引用和可变引用的「不相容」,才真正显出价值16。

思维模型:C 程序员的那套纪律,编译器接手了

原书给借用检查器找了一个出人意料的祖宗:C/C++ 程序员自己心里那套不成文的纪律—— 「别返回指向局部变量的指针」「先想清楚这块内存谁负责放」。 这套纪律的每一条,Rust 都写成了规则并执法:不是「你应该遵守」,是「不遵守就编不过」。 用原书的话说:这套流程和 C 程序员加给自己的约束没有多大不同, 区别只在于 Rust 认得这些规则,而且执行它们21。

所以学 Rust 的借用规则,有个偷懒的心法:每次编译器报错,问一句 「如果这是 C 代码,这条规则我是不是也在心里偷偷遵守着?」——十之八九,答案是肯定的; 区别是以前靠自觉,现在靠编译器。

报错文本赏析:两条经典判词

extend 那条报错的完整形态,值得读一遍原文的判词结构22:

error: cannot borrow `wave` as immutable because it is also
borrowed as mutable
 9 | extend(&mut wave, &wave);
   | ---- ^^^^- mutable borrow ends here
   | |    |
   | |    immutable borrow occurs here
   | mutable borrow occurs here

Rust 的报错不只说「不行」,还画图:哪条借用从哪开始、到哪结束、两个箭头在哪重叠。 自赋值那条同款:cannot borrow f as immutable because it is also borrowed as mutable——一句话,把「可变访问即排他」的违反现场标了出来23。

读懂报错的性价比极高:Rust 编译器以「报错写得像教学材料」著称,这不是夸奖,是事实描述。

8. 架构判例:sea of objects 与减速带

最后一个判例最大——不在代码层,在架构层。

自动内存管理(GC)普及以来,程序默认长成对象之海:对象互相随便指, 依赖关系几年后没人说得清。原书没客气:这种架构「初期进展快、容易塞东西, 几年后你会有充分理由重写(配 AC/DC 的《通往地狱的高速公路》)」; 坏处是任何东西都依赖任何东西,没法单独测试、单独演进、单独想清楚17。

Rust 的所有权模型在这里起了减速带作用:造一个环(两个值互相指)要费点劲—— 得动用 Rc 加内部可变性(还没讲的特性);Rust 偏爱指针、所有权、数据流 单向穿过系统的一棵树18。

作者给读者的当头棒喝也在这:别刚读完引用就冲出去用 Rc 连一片「结构体之海」, 复刻那套面向对象反模式——Rust 会修理你。药方是前期设计19。 全书的态度凝成一句:「Rust 就是把理解你的程序的痛苦,从未来挪到现在; 它好使得出奇——不仅逼你搞懂为什么程序线程安全,甚至逼你做高层架构设计。」20

9. 作者的判断与证据

  • 「自赋值与迭代器失效是同一类 bug」是作者的归纳,由 C++ 规范引文与 Rust 报错对照支撑, 证据链完整1012。
  • 「宽松不是免费午餐」(Java/C++ 的对照)是设计史观察,意在给 Rust 的「紧」辩护14。
  • 「按构造无数据竞争」是机制的直接推论,成立的前提是代码不含 unsafe——这个前提原书每次都会明说15。

10. 边界与局限

  • 判例集中在单线程;跨线程共享要走 Arc/Mutex 等原语,那些原语怎么配合借用规则,在未放出章节;
  • 重借(reborrow)在嵌套函数调用里的精确语义(如 Vec::push(&mut v, v.len()) 为什么其实是合法的) 本章未展开——它依赖编译器对隐式重借的处理;
  • unsafe 块里两条规则全部失效,那是「自己对自己负责」的地带。

11. 可带走的

主线例子一行版: 围绕 x 的四种借法——共享+共享 ✅、共享时再改 ❌、两个可变 ❌、可变里再借可变/共享 ✅ 但绕路碰值 ❌。

  1. 判法就两句:共享即只读(含路径冻结),可变即排他(含主人冻结);
  2. 再借顺着原引用走:共享里只能再借共享,可变里两种都行;
  3. 借用死在最后一次使用处,不是作用域末尾——早用完早解冻;
  4. extend(&mut wave, &wave) 是教科书级陷阱:容量满则搬家,旧指针悬垂;
  5. C 的 const 只封一个入口,Rust 的共享引用封全部路径——这是能查与不能查的分界;
  6. 自赋值、迭代器失效、memcpy 重叠,同一类错;可变排他一锅端;
  7. 借 HashMap 一个键,整个 map 冻结——规则不挑容器;
  8. 数据竞争的构成形状被规则预先排除:无 unsafe ⟹ 并发按构造安全;
  9. GC 养出「对象之海」;所有权模型是减速带,偏爱单向数据流;
  10. 「痛苦从未来挪到现在」:前期设计换后期调试,Rust 的交换条件。

12. 原文地图

主题 原书章 原文位置
最小判例光谱(场景 A–D) Chapter 4. References text/14-ch04-chapter-4-references.txt:811(搜「Paring」) · text/14-ch04-chapter-4-references.txt:813(搜「multiple shared borrows」)
重借规则 Chapter 4. References text/14-ch04-chapter-4-references.txt:829(搜「reborrow」) · text/14-ch04-chapter-4-references.txt:839(搜「reborrowing mutable」)
extend 向量扩容现场 Chapter 4. References text/14-ch04-chapter-4-references.txt:747(搜「extend」) · text/14-ch04-chapter-4-references.txt:769(搜「eight elements」)
两条规则原文 Chapter 4. References text/14-ch04-chapter-4-references.txt:794(搜「really frozen」)
C const 对照 Chapter 4. References text/14-ch04-chapter-4-references.txt:914(搜「pointers to const」) · text/14-ch04-chapter-4-references.txt:922(搜「constant it is not」)
File 自赋值故事 Chapter 4. References text/14-ch04-chapter-4-references.txt:852(搜「self-assignment」) · text/14-ch04-chapter-4-references.txt:872(搜「same object」)
同一类 bug 的归纳 Chapter 4. References text/14-ch04-chapter-4-references.txt:908(搜「same underlying kind」)
数据竞争按构造消失 Chapter 4. References text/14-ch04-chapter-4-references.txt:910(搜「by construction」)
HashMap 借用与宽松的代价 Chapter 4. References text/14-ch04-chapter-4-references.txt:848(搜「HashMap」) · text/14-ch04-chapter-4-references.txt:850(搜「cache-efficient」)
sea of objects Chapter 4. References text/14-ch04-chapter-4-references.txt:936(搜「sea of objects」) · text/14-ch04-chapter-4-references.txt:940(搜「Highway to Hell」)
减速带与单向数据流 Chapter 4. References text/14-ch04-chapter-4-references.txt:946(搜「speed bump」)
借用检查器的思维模型 Chapter 4. References text/14-ch04-chapter-4-references.txt:370(搜「not so different」)
两条经典报错判词 Chapter 4. References text/14-ch04-chapter-4-references.txt:783(搜「cannot borrow」)
痛苦挪到现在 Chapter 4. References text/14-ch04-chapter-4-references.txt:952(搜「transferring the pain」)

  1. 出处:「Chapter 4. References」第 811 段(text/14-ch04-chapter-4-references.txt:811,搜「Paring」)。「把这些原则削到最简单的例子。」 ↩

  2. 出处:「Chapter 4. References」第 813–821 段(text/14-ch04-chapter-4-references.txt:813-821,搜「must last」)。 ↩

  3. 出处:「Chapter 4. References」第 829–846 段(text/14-ch04-chapter-4-references.txt:829-846,搜「reborrow」)。 ↩

  4. 出处:「Chapter 4. References」第 769–773 段(text/14-ch04-chapter-4-references.txt:769-773,搜「old four-element buffer」)。 ↩

  5. 出处:「Chapter 4. References」第 775–779 段(text/14-ch04-chapter-4-references.txt:775-779,搜「invalidates」)。 ↩

  6. 出处:「Chapter 4. References」第 801–803 段(text/14-ch04-chapter-4-references.txt:801-803,搜「second rule」)。 ↩

  7. 出处:「Chapter 4. References」第 912–914 段(text/14-ch04-chapter-4-references.txt:912-914,搜「pointers to const」)。 ↩

  8. 出处:「Chapter 4. References」第 922 段(text/14-ch04-chapter-4-references.txt:922,搜「constant it is not」)。 ↩

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

  10. 出处:「Chapter 4. References」第 852–872 段(text/14-ch04-chapter-4-references.txt:852-872,搜「self-assignment」)。 ↩↩

  11. 出处:「Chapter 4. References」第 897–906 段(text/14-ch04-chapter-4-references.txt:897-906,搜「immutable because」)。 ↩

  12. 出处:「Chapter 4. References」第 908 段(text/14-ch04-chapter-4-references.txt:908,搜「same underlying kind」)。 ↩↩

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

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

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

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

  17. 出处:「Chapter 4. References」第 936–942 段(text/14-ch04-chapter-4-references.txt:936-942,搜「Highway to Hell」)。 ↩

  18. 出处:「Chapter 4. References」第 946 段(text/14-ch04-chapter-4-references.txt:946,搜「speed bump」)。 ↩

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

  20. 出处:「Chapter 4. References」第 952 段(text/14-ch04-chapter-4-references.txt:952,搜「transferring the pain」)。 ↩

  21. 出处:「Chapter 4. References」第 370 段(text/14-ch04-chapter-4-references.txt:370,搜「not so different」)。 ↩

  22. 出处:「Chapter 4. References」第 783–792 段(text/14-ch04-chapter-4-references.txt:783-792,搜「cannot borrow」)。 ↩

  23. 出处:「Chapter 4. References」第 899–906 段(text/14-ch04-chapter-4-references.txt:899-906,搜「immutable because」)。 ↩