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 后使用。

结合场景再看三个关注点

  1. 归档拿走所有权 = 前台不能再改同一条堆字符串,避免“归档中途被改”的竞态叙事。
  2. 工单号是 Copy:小标量复制便宜且无堆资源。
  3. 无需手动 freearchive_note 结束即 drop。

核心概念与准确模型

三条规则(教学浓缩)

  1. 每个值有一个所有者
  2. 同一时刻只有一个所有者
  3. 所有者离开作用域时值被丢弃

Move(移动)

对拥有堆资源的类型(如 StringVec):

let a = String::from("hi");
let b = a; // a move 到 b,a 失效

实现上通常是转移指针元数据,避免深拷贝;安全上禁止两个所有者 double-free。

Copy

实现 Copy 的类型(多数标量、不可变引用等)在赋值时按位复制,源仍可用:

let x = 5;
let y = x; // x 仍可用

CopyDrop 互斥:需要特殊析构的类型不能 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 编译期唯一主人

工程实践

  1. API:需要拿走数据就收 T;只需看就收 &T;要改就收 &mut T
  2. 大结构避免无意义 move 来回;用借用或内部引用结构。
  3. 读编译器 E0382 等 move 错误信息,按提示插入 borrow/clone/重构。

可验证实验

  1. let s = String::from("a"); let t = s; println!("{s}"); → 失败。
  2. 同上对 i32 → 成功。
  3. let s = String::from("a"); let t = s.clone(); println!("{s}{t}"); → 成功。
  4. 写一个返回 String 的函数,确认调用方接收所有权。

本节总结

  • 所有权三条规则定义资源安全。
  • 默认 move,Copy 例外,clone 显式。
  • Drop 绑定作用域。
  • 下一站是不夺取所有权的借用。

自测题

概念题

  1. 为什么 String 赋值后源不能用,而 i32 可以?

代码推理题

  1. 函数 fn take(s: String) {} 调用 take(s) 后,调用方 s 是否可用?

工程思考题

  1. 何时该接收 String 而不是 &str
参考答案
  1. String 管理堆缓冲,移动转移所有权防 double-free;i32Copy 标量。
  2. 不可用(除非 s 是 Copy,而 String 不是)。
  3. 当 API 需要拥有数据(存储、异步跨生命周期、修改并再拥有)时收 String;只读查看优先 &str

延伸阅读与资料来源

资料类型支撑内容
The Book — Understanding Ownership官方书所有权主章
The Book — What is Ownership?官方书move/copy/drop
Rustonomicon官方底层别名与安全不变量(进阶)
创建于 2026/7/15 更新于 2026/7/15