第八篇:《智能指针与内部可变性:Rust 的进阶内存管理》

在第四篇文章中,我们学习了所有权、借用和生命周期------它们构成了 Rust 内存安全的基础。但在实际开发中,有些场景用简单的栈分配和借用无法优雅地表达:需要在堆上分配数据、需要多个所有者共享同一份数据、需要在不可变引用中修改数据、需要跨线程共享所有权。这些场景需要更强大的工具------智能指针和内部可变性。本文从 Box 讲起,深入讲解 Rc、RefCell、Arc、Mutex 的使用场景与组合方式,并通过一个多所有权场景的实战帮你掌握进阶内存管理。

一、什么是智能指针?

智能指针是一种数据结构,它的行为类似指针,但拥有额外的元数据和能力。Rust 中的智能指针通常实现了两个关键 trait:

Deref:允许智能指针像引用一样使用(通过 * 解引用)。

Drop:在值离开作用域时自动执行清理逻辑。

Rust 标准库提供了多种智能指针,覆盖不同的使用场景:

二、Box:最简单的堆分配

Box 是最基础的智能指针,它将数据存储在堆上,栈上只保留一个指针。

2.1 基本用法

rust 复制代码
fn main() {
    let b = Box::new(5);
    println!("b = {}", b);         // 自动解引用
    println!("*b = {}", *b);       // 显式解引用

    // Box 离开作用域时,堆上的数据自动释放
}

2.2 使用场景一:递归类型

Rust 需要知道每个类型的大小。递归类型(如链表)如果不使用 Box,大小将无法确定:

rust 复制代码
// ❌ 编译错误:递归类型大小无限
// enum List {
//     Cons(i32, List),
//     Nil,
// }

// ✅ 使用 Box 打破递归
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    println!("链表创建成功");
}

Box 的大小是固定的(指针大小),因此 List 的大小也是确定的。

2.3 使用场景二:大对象转移所有权

当需要转移一个大对象的所有权但不想拷贝数据时,Box 可以避免栈上的大量拷贝:

rust 复制代码
fn process(data: Box<[u8; 1024 * 1024]>) {
    // 只传递指针,不拷贝 1MB 数据
    println!("处理 {} 字节", data.len());
}

fn main() {
    let big_data = Box::new([0u8; 1024 * 1024]);
    process(big_data);   // 所有权转移,但只移动指针
}

2.4 使用场景三:Trait 对象

Box 是存放 trait 对象的标准方式:

rust 复制代码
trait Animal {
    fn speak(&self);
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn speak(&self) { println!("汪汪!"); }
}

impl Animal for Cat {
    fn speak(&self) { println!("喵喵!"); }
}

fn main() {
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog),
        Box::new(Cat),
    ];

    for animal in &animals {
        animal.speak();
    }
}

三、Rc:单线程的引用计数

Rc(Reference Counting)允许多个所有者共享同一份数据。它维护一个引用计数器,当计数归零时自动释放数据。

3.1 基本用法

rust 复制代码
use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("shared data"));

    println!("引用计数: {}", Rc::strong_count(&a));   // 1

    let b = Rc::clone(&a);   // 增加引用计数
    println!("引用计数: {}", Rc::strong_count(&a));   // 2

    {
        let c = Rc::clone(&a);
        println!("引用计数: {}", Rc::strong_count(&a));   // 3
    }   // c 离开作用域

    println!("引用计数: {}", Rc::strong_count(&a));   // 2
}   // a 和 b 离开作用域,计数归零,数据释放

💡 注意:Rc::clone(&a) 不会深拷贝数据,只增加引用计数。它比 a.clone() 更清晰地表达"增加引用"的意图。

3.2 使用场景:共享不可变数据

rust 复制代码
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: Vec<Rc<Node>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: vec![],
    });

    let branch = Rc::new(Node {
        value: 5,
        children: vec![Rc::clone(&leaf)],   // 共享 leaf
    });

    // leaf 现在有 2 个所有者
    println!("leaf 引用计数: {}", Rc::strong_count(&leaf));
    println!("branch: {:?}", branch);
}

3.3 Rc 的限制

Rc 不是线程安全的。它使用非原子操作维护引用计数,在多线程中使用会导致数据竞争。多线程场景应使用 Arc。

四、RefCell:内部可变性

内部可变性是一种设计模式:即使拥有不可变引用,也允许修改内部数据。

4.1 为什么需要 RefCell?

Rust 的借用规则在编译期执行:不可变引用不能修改数据。但有些场景下,你需要在"持有不可变引用"时修改数据------例如实现观察者模式、图结构等。

RefCell 将借用检查从编译期移到运行时。它允许你在持有不可变引用时,通过 borrow_mut() 获取可变引用。

rust 复制代码
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(5);

    // 不可变借用
    println!("值: {}", data.borrow());

    // 可变借用(即使 data 本身不可变)
    *data.borrow_mut() += 10;
    println!("修改后: {}", data.borrow());
}

4.2 运行时借用检查

RefCell 在运行时检查借用规则。违反规则会导致 panic:

rust 复制代码
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(5);

    let r1 = data.borrow();
    let r2 = data.borrow();          // ✅ 多个不可变借用可以

    // let r3 = data.borrow_mut();   // ❌ 运行时 panic:已存在不可变借用
}

RefCell 的 borrow() 和 borrow_mut() 返回智能指针:

borrow() 返回 Ref

borrow_mut() 返回 RefMut

两者都在离开作用域时自动释放借用。

4.3 try_borrow:避免 panic

rust 复制代码
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(5);
    let r1 = data.borrow();

    match data.try_borrow_mut() {
        Ok(mut r) => *r += 1,
        Err(e) => println!("借用失败: {}", e),
    }
}

五、Rc<RefCell>:多所有者 + 可变访问

将 Rc 和 RefCell 组合,可以实现多个所有者共享可变数据:

rust 复制代码
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let shared = Rc::new(RefCell::new(vec![1, 2, 3]));

    let a = Rc::clone(&shared);
    let b = Rc::clone(&shared);

    // 通过 a 修改
    a.borrow_mut().push(4);

    // 通过 b 读取
    println!("b 看到的数据: {:?}", b.borrow());   // [1, 2, 3, 4]
}

5.1 实战:图结构

rust 复制代码
use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    neighbors: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let node1 = Rc::new(Node {
        value: 1,
        neighbors: RefCell::new(vec![]),
    });

    let node2 = Rc::new(Node {
        value: 2,
        neighbors: RefCell::new(vec![Rc::clone(&node1)]),
    });

    // 给 node1 添加邻居(即使 node1 被共享)
    node1.neighbors.borrow_mut().push(Rc::clone(&node2));

    println!("node1 的邻居数: {}", node1.neighbors.borrow().len());
    println!("node1 引用计数: {}", Rc::strong_count(&node1));
}

六、Arc:多线程的引用计数

Arc(Atomic Reference Counting)是 Rc 的线程安全版本。它使用原子操作维护引用计数:

rust 复制代码
use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3, 4, 5]);

    let mut handles = vec![];

    for i in 0..3 {
        let data = Arc::clone(&data);
        let handle = thread::spawn(move || {
            println!("线程 {} 看到: {:?}", i, data);
        });
        handles.push(handle);
    }

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

Rc vs Arc:

💡 原则:单线程用 Rc,多线程用 Arc。不要为了"以后可能多线程"而盲目使用 Arc。

七、Mutex:多线程的内部可变性

Mutex(互斥锁)提供跨线程的内部可变性。它确保同一时刻只有一个线程能访问数据:

rust 复制代码
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

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

    println!("最终计数: {}", *counter.lock().unwrap());   // 10
}

关键点:

lock() 返回 LockResult<MutexGuard>,获取锁可能失败(其他线程 panic 时)。

MutexGuard 是智能指针,离开作用域时自动释放锁。

Arc<Mutex> 是多线程共享可变状态的经典模式。

八、RwLock:读写锁

RwLock 允许多个读者同时访问,但写者独占:

rust 复制代码
use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let data = Arc::new(RwLock::new(vec![1, 2, 3]));

    // 多个读者
    let mut readers = vec![];
    for i in 0..3 {
        let data = Arc::clone(&data);
        readers.push(thread::spawn(move || {
            let r = data.read().unwrap();
            println!("读者 {}: {:?}", i, *r);
        }));
    }

    // 一个写者
    let data_w = Arc::clone(&data);
    let writer = thread::spawn(move || {
        let mut w = data_w.write().unwrap();
        w.push(4);
        println!("写者完成");
    });

    for r in readers { r.join().unwrap(); }
    writer.join().unwrap();

    println!("最终数据: {:?}", *data.read().unwrap());
}

Mutex vs RwLock:

九、Weak:打破循环引用

Rc 和 Arc 的循环引用会导致内存泄漏。Weak 提供弱引用,不增加强引用计数:

rust 复制代码
use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,      // 弱引用父节点
    children: RefCell<Vec<Rc<Node>>>, // 强引用子节点
}

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

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

    // 设置 leaf 的父节点为 branch(弱引用)
    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    // 通过 weak 访问父节点
    if let Some(parent) = leaf.parent.borrow().upgrade() {
        println!("leaf 的父节点值: {}", parent.value);
    }

    // branch 的强引用计数为 1(只有外部变量),不会被 leaf 持有
    println!("branch 强引用计数: {}", Rc::strong_count(&branch));
    println!("branch 弱引用计数: {}", Rc::weak_count(&branch));
}

使用原则:

父节点对子节点用 Rc(强引用)

子节点对父节点用 Weak(弱引用)

需要访问时用 upgrade() 获取临时强引用

十、小结

Box :最简单的堆分配,用于递归类型、大对象和 trait 对象。

Rc :单线程多所有者,通过引用计数共享不可变数据。

RefCell :内部可变性,将借用检查从编译期移到运行时。

Rc<RefCell> :单线程多所有者 + 可变访问。

Arc :多线程版本的 Rc,使用原子操作。

Mutex :多线程互斥访问,Arc<Mutex> 是共享可变状态的经典模式。

RwLock :读写锁,适合读多写少的场景。

Weak :弱引用,用于打破循环引用。

相关推荐
Amos_Web3 小时前
Rspack 源码解析(七):Module、Chunk 与加载树优化
前端·rust·源码阅读
柯南466820 小时前
【AI开发之Rust】第 8 课:泛型、trait 与 trait 对象
rust·编程语言
yume_sibai1 天前
12-Rust 性能优化指南(性能分析 + 内存优化 + SIMD + 并发优化 + 编译器优化 + 基准测试)
开发语言·性能优化·rust
geovindu1 天前
rust:Builder Pattern
开发语言·设计模式·rust·建造者模式
传奇开心果编程1 天前
【Rust入门知识点学与练】第33课:异步编程入门(async/await)强化
开发语言·学习·rust
传奇开心果编程1 天前
【Rust入门知识点学与练】第34课:所有权与借用
开发语言·学习·rust
柒儿吖1 天前
SSCom 重构全记录:用 Rust 把串口调试助手送上鸿蒙 PC、macOS、Windows和Linux
测试工具·rust·harmonyos
Kapaseker1 天前
Rust 为什么越来越受欢迎?开发者角度谈
rust