Rust 所有权
Rust 所有权三条规则:每个值一个所有者、所有权可转移(move)、离开作用域时 drop;以及 Copy 类型例外。
#type / concept
#status / growing
#tech / dev
#resource / rust
[!info] 关联笔记
Rust 所有权
这个概念为什么出现
没有 GC,又要避免悬垂指针与双重释放,语言必须回答:
- 谁负责释放这块内存?
- 值传给函数后,调用方还能不能用?
- 什么类型可以“按位复制”,什么类型只能“移动”?
- 离开作用域时会发生什么?
所有权是 Rust 的核心答案:在编译期静态确定资源的唯一主人。
[!abstract] 一句话理解 每个值在任意时刻只有一个所有者;赋值/传参默认移动所有权(
Copy类型例外为复制);所有者离开作用域时自动drop,无需手动 free。
最小可运行示例
场景:工单系统把客户备注所有权交给归档函数
客服工单里有一条 String 备注。归档函数需要拥有备注以便异步落库(示意:函数结束后备注不再被前台编辑)。
若归档后再让前台打印原绑定,应在编译期失败——这正是所有权要抓住的事故。
// 业务意图:
// 1. 创建工单备注;
// 2. 将备注 move 进 archive_note,由归档路径负责;
// 3. 演示 Copy 的整数工单号仍可继续使用。
// 教学点:
// - String 移动后原绑定失效;
// - i32 实现 Copy,赋值不使原变量失效;
// - 作用域结束自动释放。
fn archive_note(note: String) {
// archive_note 成为 note 的新所有者
// 业务上:这里会写入存储;示例只打印
println!("archived_note_len={}", note.len());
// 函数结束:note drop,堆上字符串释放
}
fn main() {
let ticket_id: i32 = 42; // Copy 类型
let note = String::from("customer asks refund");
// i32 复制一份给变量副本语义:ticket_id 仍可用
let ticket_id_copy = ticket_id;
println!("ticket_id={ticket_id} copy={ticket_id_copy}");
// String 移动进函数:之后不能再使用 note
archive_note(note);
// println!("{note}"); // 若取消注释:compile error, value borrowed/moved
println!("front_desk still knows ticket_id={ticket_id}");
}
建议运行:
cargo run
期望输出:
ticket_id=42 copy=42
archived_note_len=21
front_desk still knows ticket_id=42
若取消注释 println!("{note}"),编译器会报 move 后使用。
结合场景再看三个关注点
- 归档拿走所有权 = 前台不能再改同一条堆字符串,避免“归档中途被改”的竞态叙事。
- 工单号是 Copy:小标量复制便宜且无堆资源。
- 无需手动 free:
archive_note结束即 drop。
核心概念与准确模型
三条规则(教学浓缩)
- 每个值有一个所有者
- 同一时刻只有一个所有者
- 所有者离开作用域时值被丢弃
Move(移动)
对拥有堆资源的类型(如 String、Vec):
let a = String::from("hi");
let b = a; // a move 到 b,a 失效
实现上通常是转移指针元数据,避免深拷贝;安全上禁止两个所有者 double-free。
Copy
实现 Copy 的类型(多数标量、不可变引用等)在赋值时按位复制,源仍可用:
let x = 5;
let y = x; // x 仍可用
Copy 与 Drop 互斥:需要特殊析构的类型不能 Copy。
Clone
需要显式深拷贝(或类型定义的克隆)时用 .clone():
let a = String::from("hi");
let b = a.clone();
// a 与 b 都可用,但是双份堆数据
作用域与 Drop
{
let s = String::from("temp");
} // s.drop() 在此调用
RAII:资源生命周期与作用域绑定,见 Drop 与 RAII。
函数与所有权
| 动作 | 效果(非 Copy) |
|---|---|
| 传值给函数 | move 进参数 |
| 函数返回值 | move 出给调用方 |
| 只要借用 | 用引用,见下一篇 |
设计动机
- 唯一所有者 ⇒ 释放点确定
- move 默认 ⇒ 避免隐式昂贵深拷贝,也避免别名可变
- Copy 白名单 ⇒ 让小值保持符合直觉的复制语义
- 编译期检查 ⇒ 把 use-after-free 变成编译错误
边界情况与反直觉行为
- 部分移动(从结构体移出字段)会使结构体整体处于不完整状态,限制后续使用。
mut与 move 正交:可变绑定仍可被 move;不可变绑定在 move 前也不能通过它写入。- 返回引用需要生命周期,不能返回指向局部的引用(悬垂)。
- 智能指针(
Rc/Arc)改变“唯一所有者”的形状,见后续。
常见误区
[!warning] 常见误区:把 move 当成“变量坏了” 错误理解:Rust 随机让变量失效。
正确模型:所有权已转走,原名字不再代表有效所有者。
为何易错:多数语言赋值后两边都能用。
[!warning] 常见误区:到处 clone 过编译 错误理解:clone 是默认解法。
正确模型:先问能否借用、重组所有权、或拆结构;clone 是有成本的显式选择。
与相邻概念对比
| 概念 | 问题 |
|---|---|
| 所有权 | 谁拥有,是否已 move |
| 借用 | 不拥有时如何安全访问 |
| 可变性 | 绑定是否允许写 |
| GC 语言引用 | 运行时共享图 vs 编译期唯一主人 |
工程实践
- API:需要拿走数据就收
T;只需看就收&T;要改就收&mut T。 - 大结构避免无意义 move 来回;用借用或内部引用结构。
- 读编译器 E0382 等 move 错误信息,按提示插入 borrow/clone/重构。
可验证实验
let s = String::from("a"); let t = s; println!("{s}");→ 失败。- 同上对
i32→ 成功。 let s = String::from("a"); let t = s.clone(); println!("{s}{t}");→ 成功。- 写一个返回
String的函数,确认调用方接收所有权。
本节总结
- 所有权三条规则定义资源安全。
- 默认 move,
Copy例外,clone显式。 - Drop 绑定作用域。
- 下一站是不夺取所有权的借用。
自测题
概念题
- 为什么
String赋值后源不能用,而i32可以?
代码推理题
- 函数
fn take(s: String) {}调用take(s)后,调用方s是否可用?
工程思考题
- 何时该接收
String而不是&str?
参考答案
String管理堆缓冲,移动转移所有权防 double-free;i32是Copy标量。- 不可用(除非
s是 Copy,而String不是)。 - 当 API 需要拥有数据(存储、异步跨生命周期、修改并再拥有)时收
String;只读查看优先&str。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| The Book — Understanding Ownership | 官方书 | 所有权主章 |
| The Book — What is Ownership? | 官方书 | move/copy/drop |
| Rustonomicon | 官方 | 底层别名与安全不变量(进阶) |