Rust 所有权(重点)

这是整个 Rust 系列最重要的一篇。所有权(ownership)是 Rust 区别于所有语言的灵魂概念,理解了它你就理解了 Rust;没理解,代码怎么都编译不过。请慢下来,反复读这一篇,把它彻底消化。

为什么要有所有权?

先讲它要解决什么问题。内存管理历史上出现过几种方案,各有缺陷:

Rust 想要"既安全又无 GC 暂停",于是发明了所有权系统:通过编译期规则,在不引入运行时开销的前提下,自动管理内存和防止数据竞争。代价是程序员必须理解所有权——但一旦理解,写起来非常顺。

1. 三条核心规则

所有权的全部精华就这三句话:

// ===== Rust 所有权系统的三条核心规则 =====

// 1. 每个值有且只有一个"所有者"(变量)
// 2. 当所有者离开作用域,值被自动释放(drop)
// 3. 把值赋给另一个变量/传给函数时,所有权"转移"(move)

// 这三条规则在编译期检查,运行时零开销
// 不需要 GC,也不需要手动 free

记住这三条,你就能解释几乎所有 Rust 内存行为。下面我们展开讲。

2. 栈与堆:理解所有权的基础

要理解所有权,必须先理解两种内存的区别。这是其他高级语言(JS/Python)帮你隐藏的细节,但在 Rust 里必须懂:

// 理解所有权,先要理解"栈"和"堆"两种内存

// 栈(Stack):后进先出,速度快,大小固定
//   - 函数的局部变量、参数、整数、bool 都在栈上
//   - 自动分配回收(离开作用域就弹出)

// 堆(Heap):手动管理,速度较慢,大小可变
//   - String、Vec、Box 等动态数据在堆上分配
//   - 栈上只存"指向堆的指针 + 长度 + 容量"

// 例:
fn main() {
    let n = 42;             // n 的值 42 直接在栈上
    let s = String::from("hi");   // s 在栈上(存指针/长度/容量),
                                  // 实际字符 "hi" 在堆上
}

// 为什么这重要?
// 因为所有权规则主要影响"堆上的数据"。栈数据复制成本低,
// 总是直接 Copy。堆数据复制成本高(要深拷贝),所以 Rust 默认 move。

关键洞察:栈数据复制便宜(几个字节,机器指令直接拷贝),所以赋值时直接 Copy,两边都有效;堆数据复制贵(要深拷贝整个缓冲区),所以 Rust 默认不复制,而是 move(转移所有权)。这就是所有权的核心权衡。

3. move 语义(String 示例)

来看最经典的 move 示例。String 是一个堆类型——栈上存 (指针, 长度, 容量) 三元组,实际字符数据在堆上:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;          // 所有权从 s1 转移给 s2 (move)

    // println!("{}", s1);   // 编译错误!s1 已失效
    println!("{}", s2);       // OK,s2 现在拥有数据

    // 为什么这样设计?
    // String 内部是 (指针, 长度, 容量) 三元组,都指向同一块堆内存
    // 如果 s1 和 s2 都"有效",它们离开作用域时会"双重释放"同一块内存
    // 这是严重 bug(C/C++ 常见).Rust 用 move 语义从根源避免:
    //   s1 失效 -> 只有 s2 会释放,不会重复
}

// 对比:整数等"栈类型"不会 move,而是 Copy
fn copy_demo() {
    let n1 = 42;
    let n2 = n1;          // 复制(Copy),不是 move
    println!("{} {}", n1, n2);   // 两个都还能用!
}

为什么不让 s1 和 s2 同时有效?因为它们内部指针会指向同一块堆内存,当两者都离开作用域时会双重释放(double free)——这是 C/C++ 里导致安全漏洞的经典 bug。Rust 的 move 语义从根源杜绝:赋值后旧变量立即失效,只有新变量会释放。

这是新手最容易困惑的点:"我明明声明了两个变量,怎么一个不能用了?"——因为 Rust 在保护你不犯双重释放的错。

4. Copy trait:栈类型自动复制

但整数、bool 这些"栈上"的简单类型,每次赋值都 move 太麻烦了。Rust 用 Copy trait 区分:

fn main() {
    // 实现了 Copy trait 的类型:赋值时复制,不转移所有权
    // 简单类型默认都是 Copy:
    //   所有整数、浮点(i32, f64, u8 ...)
    //   布尔 bool
    //   字符 char
    //   元组(如果所有元素都是 Copy,如 (i32, f64))
    //   不可变引用 &T

    let a = 5;
    let b = a;          // Copy,a 仍可用
    println!("{} {}", a, b);

    let pair = (1, 2);
    let p2 = pair;       // Copy(pair 含两个 i32)
    println!("{:?} {:?}", pair, p2);

    // 不是 Copy 的类型(必须 move 或显式 clone):
    //   String、Vec<T>、Box<T>、HashMap 等堆类型
    //   可变引用 &mut T
    //   含以上字段的元组/结构体

    let s1 = String::from("a");
    // let s2 = s1;     // move,s1 失效
    let s2 = s1.clone(); // 想保留 s1:用 clone() 深拷贝
    println!("{} {}", s1, s2);   // 两个都可用
}

规则:实现了 Copy 的类型,赋值时自动复制,原变量仍可用。所有栈上的简单类型默认 Copy;所有堆类型(String、Vec 等)默认不 Copy。这是 Rust 编译器自动决定的,你不需要操心——但要知道这个区别。

想知道某个类型是不是 Copy,看文档或试一下就知道。Rust 标准库的数字、字符、bool、不可变引用都是 Copy;String、Vec、Box、可变引用都不是。

5. 函数传参的所有权转移

函数传参也是一种"赋值",所以同样会 move。把 String 传给函数后,原变量失效:

fn main() {
    let s = String::from("hello");
    takes_ownership(s);       // s 的所有权转移给函数
    // println!("{}", s);     // 编译错误!s 已失效

    let n = 42;
    makes_copy(n);            // n 被复制,不影响原值
    println!("n 还能用:{}", n);   // OK

    // 从函数返回值也会转移所有权
    let new_str = gives_ownership();
    println!("拿到:{}", new_str);

    // 函数接收再返回,所有权"接力"
    let s2 = String::from("hi");
    let s3 = take_and_return(s2);
    // println!("{}", s2);    // s2 失效(被传进去了)
    println!("{}", s3);       // s3 接过所有权
}

fn takes_ownership(s: String) {
    println!("我拿到:{}", s);
}   // s 离开作用域,String 内存自动释放

fn makes_copy(n: i32) {
    println!("收到:{}", n);
}

fn gives_ownership() -> String {
    String::from("从函数返回")  // 所有权转移给调用者
}

fn take_and_return(s: String) -> String {
    s     // 接进来,再还回去
}

这听起来很麻烦:"每次调用函数都要丢一个变量?"。确实如此——但 Rust 提供借用(引用)机制优雅地解决了这个问题。下一篇"借用与引用"会详细讲,这里先理解所有权转移。

当你确实想让函数拿走所有权(比如把数据"喂"给某个对象管理),传值就是对的;当你只想"借给函数读一下",就用引用。

6. Clone:显式深拷贝

如果你真的需要两份独立的堆数据,用 clone() 显式深拷贝。Rust 不会自动深拷贝——那是昂贵的操作,必须让你明确知道:

fn main() {
    let s1 = String::from("hello");

    // 方案一:clone —— 显式深拷贝(运行时开销大,但能保留两边)
    let s2 = s1.clone();
    println!("s1 = {}, s2 = {}", s1, s2);   // 两个都可用

    // clone 适合确实需要两份数据的场景
    // 但如果只是想"读一下",用引用(借用)更好——下一篇详讲

    // 注意:clone 不是免费的!对大 String/Vec 会复制整个堆数据
    // 不要在不必要的地方 clone(比如函数传参,通常传引用更优)
}

// 怎么让自定义类型支持 Copy?
#[derive(Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}

fn main2() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1;       // 因为 Point 实现了 Copy,这里复制
    println!("{} {} {} {}", p1.x, p1.y, p2.x, p2.y);
}

注意 clone() 不是免费的——对大 String/Vec 会复制整个堆缓冲区。不要在不必要的地方 clone(特别是循环里),会拖慢程序。通常的优化方向是改用引用,避免拷贝。

自定义类型如果想支持 Copy,需要所有字段都是 Copy,然后用 #[derive(Clone, Copy)] 让编译器自动实现。这样你的类型也会按 Copy 语义工作。

7. 所有权与作用域:自动释放

所有权的第二条规则——"离开作用域自动释放"——意味着 Rust 不需要手动 free,也不需要 GC:

fn main() {
    // 所有权和"作用域"紧密绑定 —— 离开作用域自动释放
    {
        let s = String::from("局部");
        println!("{}", s);
    }   // s 离开作用域,内存立即释放(调用 drop)

    // println!("{}", s);    // 编译错误!s 已不存在

    // 这意味着:不需要手动 free,不需要 GC,内存泄漏几乎不可能
    // (除非你用 Box::leak、Rc 循环引用等显式操作)

    // 一个变量同时只能有一个所有者,释放责任清晰
    let a = String::from("x");
    {
        let b = a;          // 所有权转移给 b
        println!("{}", b);
    }   // b 离开,String 释放
    // 这里 a 早已失效,b 也已释放,资源管理清清楚楚
}

这是 Rust 内存管理的核心机制,称为 RAII(资源获取即初始化)。不仅内存,文件句柄、锁、网络连接等资源,都通过同样的"离开作用域自动 drop"机制管理——只要把它们包在合适的类型里(std::fs::File、MutexGuard 等),就永远不会忘记释放。

8. 常见坑与解决方案

新手遇到的编译错误,90% 都和所有权有关。常见情况:

遇到这些错误不要慌。Rust 编译器的报错信息业界公认最友好,会精确指出哪里违反了哪条规则,甚至给出修改建议。仔细读错误信息,往往一眼就明白。

9. 所有权 vs GC vs 手动管理

一张表总结三种方案的对比:

Rust 选了一条最难但最值得的路。理解了所有权,你不仅会写 Rust,还会对内存有更深的理解,写其他语言的代码也会更严谨。

小结

这是 Rust 的灵魂。三条规则:每个值有唯一所有者、离开作用域自动释放、赋值/传参会 move。栈类型 Copy,堆类型 move。函数传参会转移所有权,这就是为什么需要"借用"——下一篇讲引用与借用,你会看到如何在不转移所有权的前提下使用值,这是日常 Rust 编程 90% 的场景。

如果这一篇没完全看懂,建议先继续读下一篇"借用与引用",两者结合理解会更深入。也可以回头再读一遍——所有权值得反复琢磨。

← 上一篇 Rust 函数

下一篇 Rust 借用与引用

✈️💬