所有权:Rust 最独特的那块地基¶
这一章回答一个问题:内存谁来清? 别的语言的答案是二选一——垃圾回收器在运行时打扫, 或者程序员手动 free。Rust 的答案是第三条路:编译期检查的所有权规则,运行时不付出任何代价。 读完你要能复述三条规则,并能解释"为什么传个值给函数,原变量就不能用了"以及 "为什么同时有两个可变引用是编译错误"。这是全书最重要的一章,后面的并发、trait、智能指针全是它的推论。
1. 先补背景:栈、堆,和"为什么主要管堆"¶
所有权管的是内存,具体说是堆(heap)上的内存。书里给了一段预备课:
栈(stack)按"后进先出"存值:进来叫压栈,出去叫弹出,只能在顶上加减。 栈上所有数据必须有编译期已知、固定的大小。大小不定或运行时才定的数据放堆:向内存分配器申请一块地方,分配器找一块够大的空位、标记已用、返回一个指针(pointer,即那块内存的地址)。指针本身大小固定,可以放在栈上1。
为什么总在堆上做文章?压栈快(位置永远是顶,不用找),堆分配要"找地方+记账"; 取数据也是栈快,因为处理器访问挨在一起的数据(栈)比跳来跳去的数据(堆)快——书里用服务员一次点完一桌再换桌打比方2。
结论先放在这:所有权体系真正要管的,是"堆上的数据什么时候释放";栈上的数据随作用域自动消失,不需要它操心。3
2. 三条规则:全章就背这三句¶
① 每个值都有一个所有者(owner)
② 一个值同一时刻只能有一个所有者
③ 所有者离开作用域,值立刻被丢弃(drop)
图说:三句话,没有任何运行时部件在执行它们——检查发生在编译期。
违反任何一条,程序编译不过。
还有一句配套的话,原书特意说在前面:所有权机制的任何部分都不会拖慢你的程序——检查全在编译期,运行时没有额外成本4。
作用域(scope,一个名字有效的代码范围)本身不新鲜:let s = "hello" 从大括号开始有效,到大括号结束失效5。新鲜的是失效那一刻发生的事。
3. String:为什么要拿它讲所有权¶
第 3 章的类型都大小固定、放栈上、拷一份成本极低。讲所有权需要一个堆上的类型,书挑了 String。
字符串字面量("hello")的内容编译期已知,直接硬编进可执行文件——快,但代价是不可变,而且大小必须在写代码时确定6。String 不同:为支持"可变、可增长",它在堆上申请内存,大小编译期未知7。这就引出内存管理的老大难:
有借有还:配对的申请和释放,恰好一次对一次。忘了还,内存泄漏;还早了,变量指向无效内存;还两次,double free——内存损坏,可能变成安全漏洞。8
Rust 的做法:不用你手动还——所有者出作用域的那一刻,Rust 自动调用一个叫 drop 的特殊函数,String 的作者把"把内存还给分配器"的代码放在 drop 里9。书里注明:这个"对象死亡即释放资源"的模式在 C++ 里叫 RAII,熟 C++ 的读者会觉得眼熟10。
4. 主线例子:let s2 = s1; 到底发生了什么¶
这是全章的主线例子,一串看似相同的赋值,走向完全不同。
情形 A:整数。
整数整个在栈上,拷贝就是复制几个字节,两个 5 互不相干11。
情形 B:String。
按情形 A 类推,你会以为"复制了一份 hello"。不是。 原书配了三张内存图,关键事实是:
String 本体(指针、长度、容量)在栈上,文本内容在堆上。s2 = s1 只复制栈上的三样——指针、长度、容量;堆上的 "hello" 一个字节都不拷。 两个变量指向同一堆内存12。
为什么不肯做深拷贝?如果堆数据很大,复制会很贵。Rust 由此立了一条全局设计原则:Rust 永远不会自动做数据的深拷贝,任何自动复制都可以认为是廉价的13。
但浅拷贝引出一个致命问题:作用域结束时,s1、s2 会各自调用 drop,同一块内存被释放两次——double free,正是第 3 节里点名的内存安全 bug14。
Rust 的解法干脆利落:let s2 = s1; 之后,s1 被判定为不再有效。 再用 s1 就是编译错误:
error[E0382]: borrow of moved value: `s1`
move occurs because `s1` has type `String`, which does not implement the `Copy` trait
value borrowed here after move
这个"复制指针 + 作废原件"的组合,别的语言叫浅拷贝(shallow copy);因为多了"作废原件"这一步,Rust 里它有个专门名字:move(移动)。我们说 s1 被移动进了 s2。15 于是只剩 s2 一个有效所有者,出作用域时只 drop 一次——问题消失。
补一条对称规则:给已有变量整体赋新值,旧值立刻被 drop。 let mut s = String::from("hello"); s = String::from("ahoy"); 第一块堆内存马上就还,不等作用域结束16。
真要深拷贝怎么办?显式调用 clone: let s2 = s1.clone(); 堆数据也复制,两个都有效。书里点了一句很"Rust"的话:看到 clone 调用,你就知道有任意代码在执行、可能有开销——它是"这里有不同寻常的事"的视觉信号17。
那情形 A 凭什么可以? 因为整数这类"编译期大小已知、纯栈上"的类型,深浅拷贝没有区别。Rust 给它们准备了一个标注:Copy trait。实现 Copy 的类型按值传递时拷贝而不移动18。实现 Copy 的:所有整数类型、bool、所有浮点、char,以及"只含 Copy 类型的元组"((i32, i32) 可以,(i32, String) 不行)。反向约束:一个类型(或它的任何部分)实现了 Drop,就不允许再标 Copy——"出作用域时需要特殊动作"和"随手复制"天然互斥19。
5. 函数传参:同一套规则,换个场景¶
传值给函数的机制和赋值一模一样:要么 move,要么 copy。
fn takes_ownership(some_string: String) { } // s 移进函数,之后 s 失效
fn makes_copy(some_integer: i32) { } // x 是 i32,拷一份,x 仍有效
返回值同理:函数返回一个 String,就是把所有权移交出去20。这套规则能跑通,但书立刻指出它的别扭处:"函数用一下值就得把所有权拿走,想用还得让它还回来"——为此还得用元组把值和数据一起吐回来(fn calculate_length(s: String) -> (String, usize)),太繁文缛节21。解法就是下一节。
6. 借用与引用:不拿走,只看看¶
引用(reference)像一个受约束的指针:它保存一个地址,顺着它能访问数据;但数据属于别人,引用保证"在我活着期间,我指向的值一定有效且类型正确"。 创建引用的动作叫借用(borrowing)——借东西的人不能据为己有22。
fn calculate_length(s: &String) -> usize { s.len() } // 参数是引用
let len = calculate_length(&s1); // 传 &s1
函数结束,参数 s 出作用域——但不 drop 任何堆内存,因为 s 没有所有权。引用不夺走、也不用还,函数签名不用为此返回任何东西23。
6.1 可变引用:一次只能有一个¶
引用默认不可变,想改借来的值要 &mut s。但可变引用有一条大限制:同一时刻,对同一个值,只能有一个可变引用。 两个就是编译错误:
书给这条限制的动机说得很透:它让 Rust 能在编译期防止 data race(数据竞争)。数据竞争三个条件:多个指针同时访问同一数据;至少一个在写;没有同步机制。 data race 导致未定义行为,运行时极难诊断;Rust 直接拒编带 data race 的代码24。
新旧 Rust 用户都会在这里挣扎——多数语言随你想改就改。但换来的收益是:"多个可变引用同时存在"这一类 bug 从"运行时的灵异现象"变成"编译期的明确报错"25。
配套规则:可变引用和不可变引用也不能共存。 两个 &s 可以(都在读,谁也影响不了谁);&s、&s 之后再 &mut s 不行——拿不可变引用的人不 expect 值突然在自己脚下变了26。有一个细节值得记住:引用的有效期到它最后一次使用为止,不是到大括号为止——不可变引用最后一次用在 println! 之后、&mut s 才创建,这段代码就合法27。
小结成两句话,书里的"引用规则"原文:
任一时刻:要么一个可变引用,要么任意多个不可变引用。引用必须始终有效。28
6.2 悬垂引用:编译器不让"指向已释放内存"的引用存在¶
悬垂指针(dangling pointer):内存已经释放或给了别人,指针却还指向它。 Rust 的承诺:引用永远不会悬垂——有引用指向某个数据,编译器就保证那数据不会比引用先离开作用域29。
报错信息(E0106,缺 lifetime 说明符)看着陌生,但核心一句把问题说尽了:"返回类型里有借来的值,但没有任何值可供它借。" 修法:直接返回 String 本身——所有权移出去,谁接谁负责 drop30。lifetime(生命周期)这个报错里冒出来的词,第 10 章专讲。
7. 切片:把"索引"换成"带担保的引用"¶
主线例子收在这一节:写一个函数,返回字符串里的第一个单词。
不用切片的写法有一个隐蔽的 bug。 函数返回空格的下标(usize):
问题是:这个 5 只在 s 不变时才有意义,但编译器不知道它和 s 的关系。 调用方拿到 5,随后 s.clear() 清空字符串——程序照样编译通过,5 变成一个指向空白的"幽灵下标"。再写个 second_word 要返回首尾两个下标,三个变量互相脱节约等于劝退31。
字符串切片(string slice)是对 String 中一段连续元素的引用,写作 &s[0..5](从 0 到 4,右端开区间)。它内部只存两样:起始位置 + 长度。于是函数签名变成:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' { return &s[0..i]; }
}
&s[..]
}
现在返回值和底层数据绑在一起了。 再演一遍刚才的事故:let word = first_word(&s); s.clear(); ——E0502:不可变借用还在(println! 还要用 word),可变借用(clear 需要)不能进来,编译不过。切片让那一类"逻辑错但不报错、事后才炸"的 bug 在编译期就不可能发生32。
三个附带的实用知识:
- 字符串字面量就是
&str:它是指向二进制文件里某处的切片,所以天然不可变33。 - 参数写成
&str而不是&String更通用——字面量、String 的切片、整个 String 的引用都能传(背后是第 15 章讲的 deref coercion)34。 - 切片的下标必须落在 UTF-8 字符边界上,切在多字节字符中间,程序会带错退出35。数组也有同款切片:
&a[1..3],类型&[i32]36。
8. 作者的判断与证据¶
- "所有权让 Rust 不做垃圾回收也能保证内存安全,且运行无开销"——核心主张,证据是全章的机制推演:三个经典 bug(double free、data race、悬垂指针)各自对应一条规则,规则在编译期执行42429。
- "新 Rustacean 会对可变引用限制感到 struggle"——作者自陈学习成本,算是给读者打预防针25。
- "Rust 永远不会自动深拷贝"——这是设计决策,不是观察;书的理由是性能可预期13。
判断(我们的,不是书里的): 所有权真正的发明不是"自动释放"(那是 RAII 已有的), 而是把"别名(alias,多个入口指向同一数据)与可变性共存"定为编译期错误—— 6.1 节的引用规则本质就是这句话。data race、悬垂指针、迭代器失效,全是它的具体形态。 学 Rust 卡住时,回到这句话比记任何规则都管用。 如果错,会错在: 它依赖编译器能静态看清"谁是别名";通过指针运算、unsafe 或部分借用模式 绕开检查的路径存在(unsafe 见第 20 章),那句"保证"在 safe Rust 内成立,出了圈不成立。
9. 边界与局限¶
- 引用有效期"到最后一次使用"的精确判定(NLL)书里只给了结论和例子,没有展开算法。
- Copy 的清单是"一般规则"而非完备列表;判断某个类型是否 Copy 要查文档19。
- 切片一节声明"假设 ASCII",UTF-8 边界的细节留给第 8 章。
- "所有权不拖慢程序"是就规则检查而言;
clone、深拷贝本身是实打实的运行时开销,别和机制开销混淆。
10. 可带走的¶
- 三条规则:每值有主;同时一主;主亡值弃(drop)。
String= 栈上(指针/长度/容量)+ 堆上(内容);s2 = s1只拷栈上三样再作废 s1——这叫 move。- 要深拷贝就显式
.clone();看到 clone 想到"可能贵"。 - Copy trait:纯栈上的类型按值传递是拷贝;带 Drop 的类型不能 Copy。
- 函数传参/返回值遵守同一套 move/copy 规则。
&T借用不拿走;&mut T一次只能有一个;可变与不可变不能共存;引用活到最后一次使用为止。- data race 三条件;Rust 在编译期拒编。
- 悬垂引用编译不过;函数要往外给引用,就得给 lifetime 说明(第 10 章)。
&str是带担保的"字符串的一部分";参数类型优先写&str。- 切片下标必须在 UTF-8 字符边界上。
11. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 栈与堆的预备课 | Chapter 4: Understanding Ownership | text/11-ch04-chapter-4-understanding-ownership.txt:31(搜「AND THE」) |
| 所有权三规则 | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:86(搜「Ownership Rules」) |
| 检查不拖慢运行时 | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:20(搜「slow down your」) |
| 申请/释放配对的难题 | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:180(搜「difficult programming problem」) |
| drop 与 RAII | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:195(搜「special function」) |
| move 与 E0382 | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:303(搜「no longer valid」) · text/11-ch04-chapter-4-understanding-ownership.txt:315(搜「borrow of moved value」) |
| clone 是视觉信号 | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:407(搜「visual indicator」) |
| Copy trait 与 Drop 互斥 | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:426(搜「Copy trait」) |
| 传参 move/copy | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:449(搜「similar to those when」) |
| 可变引用唯一 + data race | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:673(搜「one big restriction」) · text/11-ch04-chapter-4-understanding-ownership.txt:706(搜「data race」) |
| 借用有效期到最后使用 | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:757(搜「last time that reference is used」) |
| 悬垂引用 | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:780(搜「Dangling References」) |
| 下标方案的脱节 bug | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:929(搜「empties the String」) |
| 切片与 E0502 | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:1025(搜「&str」) · text/11-ch04-chapter-4-understanding-ownership.txt:1070(搜「cannot borrow」) |
| 字面量即 &str | 同上 | text/11-ch04-chapter-4-understanding-ownership.txt:1096(搜「type of s here is &str」) |
-
出处:「Chapter 4: Understanding Ownership」第 39 段(
text/11-ch04-chapter-4-understanding-ownership.txt:39,搜「parts of memory」)与第 51 段(text/11-ch04-chapter-4-understanding-ownership.txt:51,搜「returns a pointer」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 60 段(
text/11-ch04-chapter-4-understanding-ownership.txt:60,搜「faster than allocating on the heap」)与第 67 段(text/11-ch04-chapter-4-understanding-ownership.txt:67,搜「jump around less in memory」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 80 段(
text/11-ch04-chapter-4-understanding-ownership.txt:80,搜「all problems that」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 20 段(
text/11-ch04-chapter-4-understanding-ownership.txt:20,搜「slow down your」)。 ↩↩ -
出处:「Chapter 4: Understanding Ownership」第 98 段(
text/11-ch04-chapter-4-understanding-ownership.txt:98,搜「A scope is the range」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 165 段(
text/11-ch04-chapter-4-understanding-ownership.txt:165,搜「hardcoded directly into the final executable」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 169 段(
text/11-ch04-chapter-4-understanding-ownership.txt:169,搜「growable piece of」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 180 段(
text/11-ch04-chapter-4-understanding-ownership.txt:180,搜「difficult programming problem」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 195 段(
text/11-ch04-chapter-4-understanding-ownership.txt:195,搜「special function」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 200 段(
text/11-ch04-chapter-4-understanding-ownership.txt:200,搜「RAII」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 221 段(
text/11-ch04-chapter-4-understanding-ownership.txt:221,搜「pushed onto the stack」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 253 段(
text/11-ch04-chapter-4-understanding-ownership.txt:253,搜「pointer, the length, and the capacity」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 352 段(
text/11-ch04-chapter-4-understanding-ownership.txt:352,搜「automatically create」)。 ↩↩ -
出处:「Chapter 4: Understanding Ownership」第 298 段(
text/11-ch04-chapter-4-understanding-ownership.txt:298,搜「double free error」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 331 段(
text/11-ch04-chapter-4-understanding-ownership.txt:331,搜「known as a move」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 368 段(
text/11-ch04-chapter-4-understanding-ownership.txt:368,搜「the original value」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 407 段(
text/11-ch04-chapter-4-understanding-ownership.txt:407,搜「visual indicator」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 426 段(
text/11-ch04-chapter-4-understanding-ownership.txt:426,搜「Copy trait」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 432 段(
text/11-ch04-chapter-4-understanding-ownership.txt:432,搜「implemented the Drop trait」)。 ↩↩ -
出处:「Chapter 4: Understanding Ownership」第 449 段(
text/11-ch04-chapter-4-understanding-ownership.txt:449,搜「similar to those when」)与第 520 段(text/11-ch04-chapter-4-understanding-ownership.txt:520,搜「same pattern every time」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 525 段(
text/11-ch04-chapter-4-understanding-ownership.txt:525,搜「a bit tedious」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 558 段(
text/11-ch04-chapter-4-understanding-ownership.txt:558,搜「pointer in that」)与第 621 段(text/11-ch04-chapter-4-understanding-ownership.txt:621,搜「borrowing」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 613 段(
text/11-ch04-chapter-4-understanding-ownership.txt:613,搜「the String is not dropped」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 706 段(
text/11-ch04-chapter-4-understanding-ownership.txt:706,搜「data race」)与第 713 段(text/11-ch04-chapter-4-understanding-ownership.txt:713,搜「refusing to compile」)。 ↩↩ -
出处:「Chapter 4: Understanding Ownership」第 704 段(
text/11-ch04-chapter-4-understanding-ownership.txt:704,搜「struggle with」)。 ↩↩ -
出处:「Chapter 4: Understanding Ownership」第 753 段(
text/11-ch04-chapter-4-understanding-ownership.txt:753,搜「out from under them」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 757 段(
text/11-ch04-chapter-4-understanding-ownership.txt:757,搜「last time that reference is used」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 856 段(
text/11-ch04-chapter-4-understanding-ownership.txt:856,搜「At any given time」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 785 段(
text/11-ch04-chapter-4-understanding-ownership.txt:785,搜「never be dangling references」)。 ↩↩ -
出处:「Chapter 4: Understanding Ownership」第 827 段(
text/11-ch04-chapter-4-understanding-ownership.txt:827,搜「no value for it to be borrowed from」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 919 段(
text/11-ch04-chapter-4-understanding-ownership.txt:919,搜「no guarantee that it」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 1056 段(
text/11-ch04-chapter-4-understanding-ownership.txt:1056,搜「throw a compile-time error」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 1096 段(
text/11-ch04-chapter-4-understanding-ownership.txt:1096,搜「type of s here is &str」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 1120 段(
text/11-ch04-chapter-4-understanding-ownership.txt:1120,搜「more general and useful」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 1018 段(
text/11-ch04-chapter-4-understanding-ownership.txt:1018,搜「UTF-8 character boundaries」)。 ↩ -
出处:「Chapter 4: Understanding Ownership」第 1159 段(
text/11-ch04-chapter-4-understanding-ownership.txt:1159,搜「type &[i32]」)。 ↩