跳转到内容

Rust 所有权

所有权(ownership)是让 Rust 之所以成为 Rust 的那个核心概念。靠它,Rust 既不需要垃圾回收器,也不需要你手动释放内存,就能保证内存安全。刚开始觉得别扭很正常,但核心思想其实很简单。我们一步一步来。

程序运行时会把数据放在内存里。这些内存迟早要清理掉,否则程序会泄漏内存,最后崩溃。各种语言通常用两种办法处理这件事:

  • 手动管理(C/C++):由你自己释放内存。忘了释放,就会泄漏;释放得太早,就会出现崩溃和安全漏洞。
  • 垃圾回收器(Java、Go、Python):后台有个程序替你清理。很方便,但会牺牲性能和可预测性。

Rust 走的是第三条路:它借助一套关于所有权的规则,在编译时就精确地决定每个值应该在什么时候被清理。不用手动释放,也没有垃圾回收器。

整个系统就是这三条规则,其他一切都由它们推导出来:

  1. 每个值都有一个所有者(“拥有”这个值的变量)。
  2. 值在任一时刻只能有一个所有者。
  3. 当所有者离开作用域时,这个值会被自动清理。

下面逐条看看它们是怎么工作的。

规则 3:值在作用域结束时被丢弃

Section titled “规则 3:值在作用域结束时被丢弃”

作用域(scope)就是 { 和 } 之间的区域。变量的作用域一结束,Rust 就会自动释放它的值,你永远不用写 free():

fn main() {
{
let message = String::from("你好"); // message 在这里有效
println!("{message}");
} // <- 作用域结束,`message` 被自动清理
// println!("{message}"); // ❌ 错误:这里已经没有 message 了
}
▶ 动手试一试

你什么都不用释放:message 一离开作用域,Rust 就马上替你处理好了。

有意思的地方来了。看看把一个变量赋值给另一个变量时会发生什么:

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”。

如果你确实需要第二份独立的副本,就调用 .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)→ 移动。原来的变量失效。

把一个值传给函数,也会移动它,和赋值给变量一模一样:

fn take_ownership(s: String) {
println!("现在归我所有:{s}");
} // s 在这里被丢弃
fn main() {
let text = String::from("你好");
take_ownership(text);
// println!("{text}"); // ❌ 错误:text 已经被移动进函数了
}

这正是下一个主题“借用”存在的原因。借用让函数可以使用一个值,却不拿走它的所有权,这样调用之后你还能继续用它。

先自己动手试试,可以用 Rust Playground,也可以用本地项目。过程中遇到编译错误很正常:认真读一读,它们通常会直接指出发生移动的地方。

下面的程序编译不通过。修改它,让它打印 草稿 -> 草稿 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` trait
3 | let mut edited = original;
| -------- value moved here
4 | 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}");
}

输出:

草稿 -> 草稿 v2
▶ 动手试一试

let mut edited = original; 会移动这个字符串,于是 original 不再有效。.clone() 会创建第二个字符串,它有自己的内存。这样 push_str 只修改 edited,original 仍然是 草稿。

写一个函数 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 变量接住它(遮蔽了旧的那个)。这样做行得通,但总是把值传进传出很累人。这正是借用要解决的问题。

下面的程序编译不通过。修改它,让它先打印 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` trait
4 | names.push(name);
| ---- value moved here
5 | 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