编程语言研究书库

所有权:Rust 最独特的那块地基 — 第 4 章拆解

所有权: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:整数。

let x = 5;
let y = x;      // x 和 y 都是 5,都有效

整数整个在栈上,拷贝就是复制几个字节,两个 5 互不相干11。

情形 B:String。

let s1 = String::from("hello");
let s2 = s1;

按情形 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。但可变引用有一条大限制:同一时刻,对同一个值,只能有一个可变引用。 两个就是编译错误:

error[E0499]: cannot borrow `s` as mutable more than once at a time

书给这条限制的动机说得很透:它让 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。

fn dangle() -> &String {
    let s = String::from("hello");
    &s      // 返回对 s 的引用——但 s 在函数结束就 drop 了!
}

报错信息(E0106,缺 lifetime 说明符)看着陌生,但核心一句把问题说尽了:"返回类型里有借来的值,但没有任何值可供它借。" 修法:直接返回 String 本身——所有权移出去,谁接谁负责 drop30。lifetime(生命周期)这个报错里冒出来的词,第 10 章专讲。

7. 切片:把"索引"换成"带担保的引用"

主线例子收在这一节:写一个函数,返回字符串里的第一个单词。

不用切片的写法有一个隐蔽的 bug。 函数返回空格的下标(usize):

fn first_word(s: &String) -> usize { /* 找到第一个空格,返回它的位置;没有空格返回 s.len() */ }

问题是:这个 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. 可带走的

  1. 三条规则:每值有主;同时一主;主亡值弃(drop)。
  2. String = 栈上(指针/长度/容量)+ 堆上(内容);s2 = s1 只拷栈上三样再作废 s1——这叫 move。
  3. 要深拷贝就显式 .clone();看到 clone 想到"可能贵"。
  4. Copy trait:纯栈上的类型按值传递是拷贝;带 Drop 的类型不能 Copy。
  5. 函数传参/返回值遵守同一套 move/copy 规则。
  6. &T 借用不拿走;&mut T 一次只能有一个;可变与不可变不能共存;引用活到最后一次使用为止。
  7. data race 三条件;Rust 在编译期拒编。
  8. 悬垂引用编译不过;函数要往外给引用,就得给 lifetime 说明(第 10 章)。
  9. &str 是带担保的"字符串的一部分";参数类型优先写 &str。
  10. 切片下标必须在 UTF-8 字符边界上。

11. 原文地图

主题原书章原文位置
栈与堆的预备课Chapter 4: Understanding Ownershiptext/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」)

Footnotes

  1. 出处:「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」)。 ↩

  2. 出处:「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」)。 ↩

  3. 出处:「Chapter 4: Understanding Ownership」第 80 段(text/11-ch04-chapter-4-understanding-ownership.txt:80,搜「all problems that」)。 ↩

  4. 出处:「Chapter 4: Understanding Ownership」第 20 段(text/11-ch04-chapter-4-understanding-ownership.txt:20,搜「slow down your」)。 ↩ ↩2

  5. 出处:「Chapter 4: Understanding Ownership」第 98 段(text/11-ch04-chapter-4-understanding-ownership.txt:98,搜「A scope is the range」)。 ↩

  6. 出处:「Chapter 4: Understanding Ownership」第 165 段(text/11-ch04-chapter-4-understanding-ownership.txt:165,搜「hardcoded directly into the final executable」)。 ↩

  7. 出处:「Chapter 4: Understanding Ownership」第 169 段(text/11-ch04-chapter-4-understanding-ownership.txt:169,搜「growable piece of」)。 ↩

  8. 出处:「Chapter 4: Understanding Ownership」第 180 段(text/11-ch04-chapter-4-understanding-ownership.txt:180,搜「difficult programming problem」)。 ↩

  9. 出处:「Chapter 4: Understanding Ownership」第 195 段(text/11-ch04-chapter-4-understanding-ownership.txt:195,搜「special function」)。 ↩

  10. 出处:「Chapter 4: Understanding Ownership」第 200 段(text/11-ch04-chapter-4-understanding-ownership.txt:200,搜「RAII」)。 ↩

  11. 出处:「Chapter 4: Understanding Ownership」第 221 段(text/11-ch04-chapter-4-understanding-ownership.txt:221,搜「pushed onto the stack」)。 ↩

  12. 出处:「Chapter 4: Understanding Ownership」第 253 段(text/11-ch04-chapter-4-understanding-ownership.txt:253,搜「pointer, the length, and the capacity」)。 ↩

  13. 出处:「Chapter 4: Understanding Ownership」第 352 段(text/11-ch04-chapter-4-understanding-ownership.txt:352,搜「automatically create」)。 ↩ ↩2

  14. 出处:「Chapter 4: Understanding Ownership」第 298 段(text/11-ch04-chapter-4-understanding-ownership.txt:298,搜「double free error」)。 ↩

  15. 出处:「Chapter 4: Understanding Ownership」第 331 段(text/11-ch04-chapter-4-understanding-ownership.txt:331,搜「known as a move」)。 ↩

  16. 出处:「Chapter 4: Understanding Ownership」第 368 段(text/11-ch04-chapter-4-understanding-ownership.txt:368,搜「the original value」)。 ↩

  17. 出处:「Chapter 4: Understanding Ownership」第 407 段(text/11-ch04-chapter-4-understanding-ownership.txt:407,搜「visual indicator」)。 ↩

  18. 出处:「Chapter 4: Understanding Ownership」第 426 段(text/11-ch04-chapter-4-understanding-ownership.txt:426,搜「Copy trait」)。 ↩

  19. 出处:「Chapter 4: Understanding Ownership」第 432 段(text/11-ch04-chapter-4-understanding-ownership.txt:432,搜「implemented the Drop trait」)。 ↩ ↩2

  20. 出处:「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」)。 ↩

  21. 出处:「Chapter 4: Understanding Ownership」第 525 段(text/11-ch04-chapter-4-understanding-ownership.txt:525,搜「a bit tedious」)。 ↩

  22. 出处:「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」)。 ↩

  23. 出处:「Chapter 4: Understanding Ownership」第 613 段(text/11-ch04-chapter-4-understanding-ownership.txt:613,搜「the String is not dropped」)。 ↩

  24. 出处:「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」)。 ↩ ↩2

  25. 出处:「Chapter 4: Understanding Ownership」第 704 段(text/11-ch04-chapter-4-understanding-ownership.txt:704,搜「struggle with」)。 ↩ ↩2

  26. 出处:「Chapter 4: Understanding Ownership」第 753 段(text/11-ch04-chapter-4-understanding-ownership.txt:753,搜「out from under them」)。 ↩

  27. 出处:「Chapter 4: Understanding Ownership」第 757 段(text/11-ch04-chapter-4-understanding-ownership.txt:757,搜「last time that reference is used」)。 ↩

  28. 出处:「Chapter 4: Understanding Ownership」第 856 段(text/11-ch04-chapter-4-understanding-ownership.txt:856,搜「At any given time」)。 ↩

  29. 出处:「Chapter 4: Understanding Ownership」第 785 段(text/11-ch04-chapter-4-understanding-ownership.txt:785,搜「never be dangling references」)。 ↩ ↩2

  30. 出处:「Chapter 4: Understanding Ownership」第 827 段(text/11-ch04-chapter-4-understanding-ownership.txt:827,搜「no value for it to be borrowed from」)。 ↩

  31. 出处:「Chapter 4: Understanding Ownership」第 919 段(text/11-ch04-chapter-4-understanding-ownership.txt:919,搜「no guarantee that it」)。 ↩

  32. 出处:「Chapter 4: Understanding Ownership」第 1056 段(text/11-ch04-chapter-4-understanding-ownership.txt:1056,搜「throw a compile-time error」)。 ↩

  33. 出处:「Chapter 4: Understanding Ownership」第 1096 段(text/11-ch04-chapter-4-understanding-ownership.txt:1096,搜「type of s here is &str」)。 ↩

  34. 出处:「Chapter 4: Understanding Ownership」第 1120 段(text/11-ch04-chapter-4-understanding-ownership.txt:1120,搜「more general and useful」)。 ↩

  35. 出处:「Chapter 4: Understanding Ownership」第 1018 段(text/11-ch04-chapter-4-understanding-ownership.txt:1018,搜「UTF-8 character boundaries」)。 ↩

  36. 出处:「Chapter 4: Understanding Ownership」第 1159 段(text/11-ch04-chapter-4-understanding-ownership.txt:1159,搜「type &[i32]」)。 ↩

On this page