Rust 所有权
Rust 值的所有者、移动与 Copy 语义,以及通常的析构时机和共享所有权边界。
[!info] related notes
Rust 所有权
这个概念为什么出现
没有 GC,又要避免悬垂指针与双重释放,语言必须回答:
- 谁负责释放这块内存?
- 值传给函数后,调用方还能不能用?
- 什么类型可以“按位复制”,什么类型只能“移动”?
- 离开作用域时会发生什么?
所有权是 Rust 的核心答案:在类型与借用规则中约束值的持有、转移和访问。
[!abstract] 一句话理解 每个值在任意时刻只有一个所有者;赋值/传参默认移动所有权(
Copy类型例外为复制);仍持有的值通常在离开作用域时析构,无需手动 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=20
front_desk still knows ticket_id=42
若取消注释 println!("{note}"),编译器会报 move 后使用。
结合场景再看三个关注点
- 原绑定在 move 后不可访问:这说明 String 的所有权转移,不等于数据库归档已成功,也不是完整的并发控制。
- 工单号是 Copy:小标量复制便宜且无堆资源。
- 无需手动 free:
archive_note结束即 drop。
核心概念与准确模型
三条规则(教学浓缩)
- 每个值有一个所有者
- 同一时刻只有一个所有者
- 所有者离开作用域时值被丢弃
Move(移动)
未实现 Copy 的值在传值时移动,不限于堆分配类型;以下以 String 为例:
let a = String::from("hi");
let b = a; // a move 到 b,a 失效
对 String/Vec,move 不克隆其堆缓冲区;可能转移指针、长度、容量等表示,或被编译器优化掉。一般类型的 move 不保证是指针交换,也不等于堆分配。
Copy
实现 Copy 的类型(多数标量、不可变引用等)在赋值时按位复制,源仍可用:
let x = 5;
let y = x; // x 仍可用
Copy 与 Drop 互斥:需要特殊析构的类型不能 Copy。
Clone
需要类型定义的显式克隆时用 .clone():String 克隆复制字节,Rc/Arc 克隆则共享底层分配并增加引用计数,不能统一称为深拷贝。
let a = String::from("hi");
let b = a.clone();
// a 与 b 都可用,但是双份堆数据
作用域与 Drop
{
let s = String::from("temp");
} // s 在此自动析构;不能显式调用 s.drop(),提前释放用 drop(s)
普通作用域退出会析构;mem::forget、引用计数环、进程退出或 abort 等情况可能不运行析构,Rust 不保证绝无资源泄漏。RAII 详见 Drop 与 RAII。
函数与所有权
| 动作 | 效果(非 Copy) |
|---|---|
| 传值给函数 | move 进参数 |
| 函数返回值 | move 出给调用方 |
| 只要借用 | 用引用,见下一篇 |
设计动机
- 唯一所有者 ⇒ 释放点确定
- move 默认 ⇒ 避免隐式昂贵深拷贝,也避免别名可变
- Copy 白名单 ⇒ 让小值保持符合直觉的复制语义
- 编译期检查 ⇒ 把 use-after-free 变成编译错误
边界情况与反直觉行为
- 部分移动(从结构体移出字段)会使结构体整体处于不完整状态,限制后续使用。
mut与 move 正交:可变绑定仍可被 move;不可变绑定在 move 前也不能通过它写入。- 返回引用需要生命周期,不能返回指向局部的引用(悬垂)。
- 共享所有权:每个 Rc/Arc 句柄仍是受 move/drop 规则约束的值,但多个句柄共同持有同一分配;最后一个强引用释放时析构底层值。
常见误区
[!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标量。- 不可用;这里的 String 不是 Copy,调用已转移所有权。
- 当 API 需要拥有数据(存储、异步跨生命周期、修改并再拥有)时收
String;只读查看优先&str。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| The Book — Understanding Ownership | 官方书 | 所有权主章 |
| The Book — What is Ownership? | 官方书 | move/copy/drop |
| Rustonomicon | 官方 | 底层别名与安全不变量(进阶) |