Rust 所有权
所有权(ownership)是让 Rust 之所以成为 Rust 的那个核心概念。靠它,Rust 既不需要垃圾回收器,也不需要你手动释放内存,就能保证内存安全。刚开始觉得别扭很正常,但核心思想其实很简单。我们一步一步来。
所有权要解决什么问题
Section titled “所有权要解决什么问题”程序运行时会把数据放在内存里。这些内存迟早要清理掉,否则程序会泄漏内存,最后崩溃。各种语言通常用两种办法处理这件事:
- 手动管理(C/C++):由你自己释放内存。忘了释放,就会泄漏;释放得太早,就会出现崩溃和安全漏洞。
- 垃圾回收器(Java、Go、Python):后台有个程序替你清理。很方便,但会牺牲性能和可预测性。
Rust 走的是第三条路:它借助一套关于所有权的规则,在编译时就精确地决定每个值应该在什么时候被清理。不用手动释放,也没有垃圾回收器。
所有权的三条规则
Section titled “所有权的三条规则”整个系统就是这三条规则,其他一切都由它们推导出来:
- 每个值都有一个所有者(“拥有”这个值的变量)。
- 值在任一时刻只能有一个所有者。
- 当所有者离开作用域时,这个值会被自动清理。
下面逐条看看它们是怎么工作的。
规则 3:值在作用域结束时被丢弃
Section titled “规则 3:值在作用域结束时被丢弃”作用域(scope)就是 { 和 } 之间的区域。变量的作用域一结束,Rust 就会自动释放它的值,你永远不用写 free():
fn main() { { let message = String::from("你好"); // message 在这里有效 println!("{message}"); } // <- 作用域结束,`message` 被自动清理
// println!("{message}"); // ❌ 错误:这里已经没有 message 了}你什么都不用释放:message 一离开作用域,Rust 就马上替你处理好了。
规则 1 和 2:“移动”
Section titled “规则 1 和 2:“移动””有意思的地方来了。看看把一个变量赋值给另一个变量时会发生什么:
fn main() { let s1 = String::from("你好"); let s2 = s1; // 所有权从 s1 移动到 s2
println!("{s2}"); // ✅ 没问题 // println!("{s1}"); // ❌ 错误:s1 已经被移动了}写下 let s2 = s1 时,Rust 并不会复制这个字符串,而是把所有权从 s1 移动(move)到 s2。规则 2 说一个值只能有一个所有者,所以 s1 从此失效。如果还想用它,就会得到这个错误:
error[E0382]: borrow of moved value: `s1`意思是“借用了已被移动的值 s1”。
如果我就是想要两份呢?
Section titled “如果我就是想要两份呢?”如果你确实需要第二份独立的副本,就调用 .clone():
fn main() { let s1 = String::from("你好"); let s2 = s1.clone(); // 完整复制一份
println!("s1 = {s1}, s2 = {s2}"); // ✅ 现在两个都能用}clone() 必须明确写出来,这是故意的:复制可能很耗资源,所以 Rust 要你主动提出来。
简单类型会被复制,而不是移动
Section titled “简单类型会被复制,而不是移动”你可能会好奇,为什么下面这段代码不会报错:
fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); // ✅ 两个都能用}整数、浮点数、布尔值和 char 这类大小固定的小值,复制起来很便宜,所以 Rust 直接复制它们,而不是移动。作为新手,记住这条经验就够了:
- 简单值(数字、
bool、char)→ 复制。原来的变量依然有效。 - 拥有堆数据的值(比如
String)→ 移动。原来的变量失效。
所有权与函数
Section titled “所有权与函数”把一个值传给函数,也会移动它,和赋值给变量一模一样:
fn take_ownership(s: String) { println!("现在归我所有:{s}");} // s 在这里被丢弃
fn main() { let text = String::from("你好"); take_ownership(text); // println!("{text}"); // ❌ 错误:text 已经被移动进函数了}这正是下一个主题“借用”存在的原因。借用让函数可以使用一个值,却不拿走它的所有权,这样调用之后你还能继续用它。
先自己动手试试,可以用 Rust Playground,也可以用本地项目。过程中遇到编译错误很正常:认真读一读,它们通常会直接指出发生移动的地方。
1. 保留原来的字符串
Section titled “1. 保留原来的字符串”下面的程序编译不通过。修改它,让它打印 草稿 -> 草稿 v2,而且两个变量都不能删掉。
fn main() { let original = String::from("草稿"); let mut edited = original; edited.push_str(" v2"); println!("{original} -> {edited}");}error[E0382]: borrow of moved value: `original` --> src/main.rs:5:16 |2 | let original = String::from("草稿"); | -------- move occurs because `original` has type `String`, which does not implement the `Copy` trait3 | let mut edited = original; | -------- value moved here4 | edited.push_str(" v2");5 | println!("{original} -> {edited}"); | ^^^^^^^^ value borrowed here after move提示:你需要两个互相独立的字符串,这样改其中一个时,另一个不受影响。
查看答案
fn main() { let original = String::from("草稿"); let mut edited = original.clone(); // 完整、独立的一份副本 edited.push_str(" v2"); println!("{original} -> {edited}");}输出:
草稿 -> 草稿 v2let mut edited = original; 会移动这个字符串,于是 original 不再有效。.clone() 会创建第二个字符串,它有自己的内存。这样 push_str 只修改 edited,original 仍然是 草稿。
2. 再还回来
Section titled “2. 再还回来”写一个函数 add_signature,它获取一个 String 的所有权,在末尾加上 " -- Ferris",再把字符串交还回去。在 main 里调用它并打印结果。
提示:函数可以把收到的值作为返回值交出去。在签名里写上 -> String,并把参数写成 mut text: String,这样才允许修改它。
查看答案
fn add_signature(mut text: String) -> String { text.push_str(" -- Ferris"); text // 所有权移动回调用者}
fn main() { let letter = String::from("回头见!"); let letter = add_signature(letter); println!("{letter}");}输出:
回头见! -- Ferris调用函数时,所有权移进函数;返回时,所有权又随着返回值移出来。我们用一个新的 letter 变量接住它(遮蔽了旧的那个)。这样做行得通,但总是把值传进传出很累人。这正是借用要解决的问题。
3. 现在归谁所有?
Section titled “3. 现在归谁所有?”下面的程序编译不通过。修改它,让它先打印 Ana 加入了名单,再打印 ["Ana"]。
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); names.push(name); println!("{name} 加入了名单"); println!("{:?}", names);}error[E0382]: borrow of moved value: `name` --> src/main.rs:5:16 |3 | let name = String::from("Ana"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait4 | names.push(name); | ---- value moved here5 | println!("{name} 加入了名单"); | ^^^^ value borrowed here after move提示:push 会获取所有权,和函数调用一样。第 4 行之后,这个字符串归谁所有?要么在移动之前使用 name,要么去问新的所有者。
查看答案
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); println!("{name} 加入了名单"); // 趁还拥有它的时候使用 names.push(name); // 现在归 vector 所有 println!("{:?}", names);}输出:
Ana 加入了名单["Ana"]执行 names.push(name) 之后,vector 成了所有者,name 就不能再用了。先打印是一种修法。另一种是保持原来的顺序,改成打印 names[0],因为字符串还在 vector 里。克隆也行,但这里没必要复制。
- 所有权让 Rust 不需要垃圾回收器也能安全地管理内存。
- 三条规则:每个值有一个所有者;同一时刻只能有一个所有者;所有者离开作用域时,值被丢弃。
- 赋值拥有堆数据的值(比如
String)时会移动它,原来的变量随之失效。 - 需要独立副本时用
.clone();简单类型(数字、bool、char)会自动复制。 - 把值传给函数也会移动它,这就引出了下一课:借用。
小测验
1. 在 Rust 中,一个值在同一时刻可以有几个所有者?
2. 想得到一个 String 的独立副本而不是移动它,应该调用哪个方法?
3. 下面哪种类型在赋值给另一个变量时会自动复制,而不是移动?
4. 把一个 String 传给按值(而不是按引用)接收它的函数,会发生什么?
5. 根据本页内容,Rust 什么时候会自动清理(丢弃)一个值?
得分: 0 / 5
- 什么是所有权? — Rust 程序设计语言:官方教程的章节,用图解释
String在内存里长什么样,以及为什么移动不会复制数据。 - 所有权和移动 — 通过例子学 Rust:简短、可运行的移动示例,包括把值移动进函数。
- RAII — 通过例子学 Rust:值离开作用域时怎样被清理,以及
Droptrait 在其中的作用。 Copy— 标准库文档(英文):哪些类型会被复制而不是移动,以及Copy和Clone的区别。- Exercism 的 Rust 练习(英文):想做比本页更多的练习时,这里有带反馈的免费练习题。