Skip to content
Published at:

04. Ownership & Moves — 所有权与移动

所有权(ownership)是 Rust 最独特、最重要的特性。它是 Rust 在不使用垃圾回收的前提下实现内存安全的基石。本章深入所有权和移动语义——这是 Rust 学习者与编译器「搏斗」最频繁的领域,也是掌握 Rust 的分水岭。

内存管理的三难选择

先回顾编程语言管理内存的三种路线:

路线代表语言安全性能精细控制
手动管理C, C++易出错极快完全控制
垃圾回收Java, Go, Python安全有 GC 停顿弱控制
编译期所有权Rust安全极快完全控制

手动管理给了你绝对的控制权,但你必须记住所有细节——悬垂指针、双重释放、use-after-free。在大项目中,依靠人类意志力保证绝对正确已被证明是不可行的。

垃圾回收让你忘记内存管理,但它接管了内存释放的唯一权限——你无法精确控制对象何时释放,也无法避免 GC 的 stop-the-world 暂停。

Rust 的第三条路是:在编译期追踪所有权,当值的所有者离开作用域时自动释放。没有 GC 运行时开销,也没有悬垂指针。

所有权规则

Rust 的所有权规则只有三条,但它们驱动了整个语言的设计:

  1. Rust 中的每一个值都有一个所有者(owner)。
  2. 同一时间,一个值只能有一个所有者。
  3. 当所有者离开作用域时,值被丢弃(drop)。

最简单的例子:

rust
{
    let s = String::from("hello");
    // s 是 "hello" 的所有者
}   // s 离开作用域,"hello" 被释放

String 的值「hello」存储在堆上——它的结构是一个三词组的栈上控制块(指针、长度、容量),加上堆上的字符数组。当 s 离开作用域时,Rust 自动调用 sdrop 方法,释放堆上的字符数组。

对于大多数类型,这个过程是递归的:释放一个 Vec<String> 会先释放每个 String 中的堆缓冲区,再释放 Vec 本身的堆数组。

所有权树

所有权形成了一个树状结构:每个值有一个所有者,所有者可能本身就是一个拥有子值的复合值。用代码来阐释:

rust
struct Book {
    title: String,   // Book 拥有 title
    author: String,  // Book 拥有 author
    pages: u32,
}

struct Library {
    name: String,            // Library 拥有 name
    books: Vec<Book>,       // Library 拥有 books(一个 Vec)
}

{
    let library = Library {
        name: String::from("City Library"),
        books: vec![
            Book {
                title: String::from("Programming Rust"),
                author: String::from("Jim Blandy"),
                pages: 735,
            },
        ],
    };
    // 所有权树:
    // library ─┬─ name ──── "City Library"(堆)
    //           └─ books ─── Vec<Book>
    //                           └─ [0] ─┬─ title ── "Programming Rust"(堆)
    //                                    ├─ author ── "Jim Blandy"(堆)
    //                                    └─ pages ── 735(栈)
}   // library 离开作用域,整棵树递归释放

library 离开作用域时,Rust 首先丢弃 books(它释放了 Vec 的堆数组并丢弃其所有元素),然后丢弃 name,最后丢弃 library 自身的栈帧。整个过程是确定的、可预测的——没有 GC 扫描,没有引用计数遍历(除非显式使用 Rc)。

对于 C++ 程序员,这个行为和 C++ RAII 类似,但有编译器的强制保证;对于 Java/Go/Python 程序员,这是 GC 提供的安全性,但发生在确定的时间点(作用域结束时),而不是 GC 选择的不确定时刻。

移动语义

在 Rust 中,赋值操作的行为取决于值的类型:

rust
// 情况 A:简单类型 —— 拷贝
let x = 42;
let y = x;      // x 的值被拷贝到 y
println!("{}", x);  // OK:x 仍然有效

// 情况 B:复杂类型 —— 移动
let s1 = String::from("hello");
let s2 = s1;        // s1 的所有权被移动到 s2
// println!("{}", s1);  // 编译错误:s1 已被移动,不能使用

对于 String,赋值转移了所有权。这是因为 String 在堆上管理缓冲区——如果像整数那样按位复制,两个变量将指向同一个堆缓冲区,离开作用域时会造成双重释放。

Rust 的解决方案:赋值 = 所有权转移 + 源变量失效。此后 s1 被视为「未初始化」,编译器禁止任何对 s1 的访问。

编译器的报错直截了当:

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:20
  |
3 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
4 |     let s2 = s1;
  |              -- value moved here
5 |     println!("{}", s1);
  |                    ^^ value borrowed here after move

不仅仅是直接赋值——以下所有操作都涉及移动:

rust
let s = String::from("hello");

// 传递给函数会转移所有权
fn consume(s: String) { /* s 在这里被丢弃 */ }
consume(s);
// println!("{}", s);  // 错误:s 已被移动

// 从函数返回会转移所有权
fn produce() -> String {
    let s = String::from("world");
    s   // 所有权从 s 转移到调用者
}
let s2 = produce();  // s2 是新的所有者

// 赋值到复合数据结构
let mut v = Vec::new();
v.push(String::from("item"));   // String 的所有权转移到 Vec

// 从复合数据结构取出(需要通过特殊方式)
let last = v.pop().unwrap();    // pop 返回 Option<String>,转移所有权

移动与控制流

移动语义与控制流的交互需要特别关注:

rust
let x = String::from("hello");

// if 中可以在分支间转移
if true {
    let y = x;      // x 移动到 y
    // x 在此分支中不可用
} else {
    // x 在此分支中仍可用
}

// 但之后 x 的状态不确定
// println!("{}", x);  // 错误:可能已被移动

在循环中移动尤为危险:

rust
let v = vec![String::from("a"), String::from("b")];

// 这个 for 循环有效——它消费了 v
for s in v {
    println!("{}", s);
}

// println!("{:?}", v);  // 错误:v 已在 for 循环中被移动

显式的 into_iter() 展示了其本质:for s in v 等价于 for s in v.into_iter(),后者将 v 的所有权转移给迭代器,迭代器在每个步骤中移出元素。

你不能在循环体内部移动值,除非每次迭代后重新赋值:

rust
let mut x = String::from("hello");
loop {
    let y = x;           // x 移动到 y
    // ... 使用 y ...
    x = String::from("world");  // x 必须被重新赋值
}

无法从集合中按索引移出

当你尝试从 Vec 中按索引取元素时:

rust
let v = vec![String::from("a"), String::from("b")];

// let first = v[0];  // 编译错误:cannot move out of index of `Vec<String>`

原因:v[0] 在语法上看起来像访问,但它试图将 v 的第一个元素的所有权移出——而 Rust 没有能力追踪「v 中索引 0 的位置现在为空」。如果允许这种操作,v 会处于部分初始化的不一致状态。

三种从集合中移出元素的惯用技巧:

技巧一:pop()

rust
let mut v = vec![String::from("a"), String::from("b"), String::from("c")];
while let Some(item) = v.pop() {
    println!("{}", item);
}
// v 现在是空的

pop() 返回 Option<T>,当向量非空时从末尾移出一个元素。被移除的槽被缩短的 len 隐性丢弃。

技巧二:swap_remove()

rust
let mut v = vec![
    String::from("a"),
    String::from("b"),
    String::from("c"),
];

let second = v.swap_remove(1);  // 将索引 1 与最后一个元素交换,然后弹出
// v 现在是 ["a", "c"]
println!("{}", second);  // "b"

swap_remove(i) 将索引 i 与最后一个元素交换,再弹出末尾。O(1) 时间,但不保留相对顺序。

技巧三:std::mem::replace()

rust
use std::mem;

let mut v = vec![
    String::from("a"),
    String::from("b"),
    String::from("c"),
];

// 用默认值替换,获取原值
let taken = mem::replace(&mut v[1], String::new());
println!("{}", taken);  // "b"
println!("{:?}", v);    // ["a", "", "c"]

mem::replace(dest, src)src 写入 dest 并返回 dest 的旧值。更通用的版本是 mem::take(dest),等价于 mem::replace(dest, T::default())

rust
let taken = mem::take(&mut v[0]);
println!("{}", taken);  // "a"
println!("{:?}", v);    // ["", "", "c"]

Copy 类型:规则的例外

到目前为止,所有移动例子都涉及 String。但对于整数、浮点、布尔、char 等简单类型,赋值行为不同:

rust
let x = 42;
let y = x;          // x 被拷贝,不是移动
println!("{}", x);  // OK

这些类型实现了 Copy Trait。对于 Copy 类型,赋值执行按位复制,原值继续有效:

rust
// 以下类型都是 Copy
let a: i32 = 1;
let b: f64 = 3.14;
let c: bool = true;
let d: char = 'A';
let e: (i32, i32) = (1, 2);     // 由 Copy 类型组成的元组是 Copy
let f: [i32; 3] = [1, 2, 3];    // 由 Copy 类型组成的数组是 Copy
let g: &str = "hello";           // 不可变引用是 Copy

完整的标准 Copy 类型列表:

类型类别Copy 类型
所有整数i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize
浮点数f32 f64
布尔bool
字符char
共享引用&T(所有 &T,甚至对非 Copy 类型的引用)
裸指针*const T *mut T
函数指针fn(T) -> R
由 Copy 类型构成的组合(i32, f64) [u8; 1024] Option<&T>

哪些类型不是 Copy?

  • String:拥有堆缓冲区
  • Vec<T>:拥有堆缓冲区
  • Box<T>:拥有堆分配
  • &mut T:可变引用不是 Copy(保证独占性)
  • 大多数复合结构(除非显式派生 Copy

为自定义类型派生 Copy

rust
#[derive(Copy, Clone)]
struct Point {
    x: f64,
    y: f64,
}

let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1;      // p1 被拷贝
println!("{}", p1.x);  // OK

约束:只有当结构体的所有字段都是 Copy 时,才能派生 Copy。如果结构体包含 String 字段,就无法 Copy——这保证了 Rust 的「按位复制即安全」的前提条件。

Rc 和 Arc:共享所有权

所有权规则说一个值只能有一个所有者。但在某些场景下,我们确实需要共享所有权——比如 DOM 树中的节点被多处引用。Rust 通过引用计数提供了共享所有权:

Rc<T> —— 单线程引用计数

rust
use std::rc::Rc;

let s = Rc::new(String::from("shared"));
assert_eq!(Rc::strong_count(&s), 1);

let t = Rc::clone(&s);   // 增加引用计数,不克隆底层数据
assert_eq!(Rc::strong_count(&s), 2);

// s 和 t 都可以读取底层数据
println!("{}", s);
println!("{}", t);
// 当 s 和 t 都离开作用域时,String 被释放

Rc::clone() 不会深拷贝数据——它只增加引用计数。当最后一个 Rc 指针被丢弃时,底层数据被释放。

Rc<T> 提供的引用是不可变的:

rust
let s = Rc::new(String::from("hello"));
// s.push_str(" world");  // 错误:不能可变借用 Rc 的内容

Rc 故意不支持可变性:如果允许多个位置持有的数据可变,就可能引入数据竞争或迭代器失效问题。对于需要可变的场景,通常结合 Rc<RefCell<T>> 使用(在后续章节介绍)。

Rc<T> 的内存布局

Rc::new(String::from("hello")) 分配了两块内存:
┌──────────────────────┐
│  强引用计数 = 1       │  ← 引用计数块(堆上)
│  弱引用计数 = 0       │
│  ┌──────────────────┐│
│  │ ptr │ len │ cap ││  ← String 控制块
│  │  ↓               ││
│  │"h""e""l""l""o"  ││  ← 字符数据
│  └──────────────────┘│
└──────────────────────┘

栈上变量 s:1 word 指向引用计数块
栈上变量 t (clone):1 word 指向同一个引用计数块

Arc<T> —— 多线程引用计数

rust
use std::sync::Arc;
use std::thread;

let s = Arc::new(String::from("shared across threads"));
let mut handles = vec![];

for _ in 0..3 {
    let s = Arc::clone(&s);
    handles.push(thread::spawn(move || {
        println!("{}", s);
    }));
}

for h in handles {
    h.join().unwrap();
}

Arc(Atomic Reference Count)与 Rc 功能相同,但使用原子操作来更新引用计数——在多线程间安全,但有轻微的性能成本。

引用循环与 Weak<T>

rust
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    parent: RefCell<Weak<Node>>,     // 弱引用,避免循环
    children: RefCell<Vec<Rc<Node>>>,
}

let leaf = Rc::new(Node {
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![]),
});

let branch = Rc::new(Node {
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![Rc::clone(&leaf)]),
});

// 建立反向引用,使用 Weak 避免计数循环
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);

// 验证:branch 有两个强引用(自己 + leaf.children),leaf 有一个强引用(自己)
println!("branch strong count: {}", Rc::strong_count(&branch));  // 1 或 2
println!("leaf strong count: {}", Rc::strong_count(&leaf));      // 1 或 2

Weak<T> 不增加强引用计数——当所有 Rc 指针被丢弃后,数据被释放,Weak 变为无效(upgrade() 返回 None)。这在树结构、缓存等场景中至关重要。

所有权的哲学

Rust 的所有权系统初看起来像限制,但我们应该重新理解它:

  • 所有权不是枷锁,而是文档。 函数签名 fn consume(s: String) 明确传达了「此函数会消耗这个字符串」的信息。在 C++ 中,你只能依靠注释或文档。
  • 所有权是编译器提供的安全网。 你不会忘记释放内存,因为编译器不会忘记调用 drop。你不会意外使用已释放的指针,因为编译器在编译时就阻止了这些操作。
  • 所有权使并发成为可能。 SendSync Trait(自动派生)检查类型的所有权结构,确保跨线程传递不会引入数据竞争。
  • 所有权强制了清晰的 API 设计。 当所有权规则让你感到受限时,通常意味着你的 API 设计需要改进——也许应该用借用(&T)而不是消耗所有权(T)。

小结

  • 所有权三规则:每个值一个所有者;同一时间只有一个所有者;离开作用域时释放。
  • 所有权形成树状结构:释放根节点递归释放整个子树,类似于 RAII,但有编译器强制保证。
  • 移动语义是默认行为:非 Copy 类型的赋值转移所有权,源变量失效。这避免了双重释放和 use-after-free。
  • 编译错误信息精确error[E0382]: borrow of moved value 直接指出问题所在。
  • 控制流中的移动需谨慎if 分支中一个分支移出后,源变量在该分支外状态不确定;循环体内移出需要每次迭代后重新赋值。
  • 不能从 Vec 中按索引移出:三种惯用替代——pop()(末尾移出)、swap_remove(i)(交换后移出)、std::mem::replace() / std::mem::take()(替换后取旧值)。
  • Copy 类型是例外:简单类型(整数、浮点、boolchar、共享引用)赋值时按位复制。Clone 是显式的深拷贝。
  • Rc<T>Arc<T> 提供引用计数式的共享所有权(不可变)。Weak<T> 避免循环引用。Arc 使用原子操作,适合多线程。