Rust:智能指针Box/Rc/Arc详解

Rust的所有权系统在编译期就杜绝了大多数内存问题,但有些场景需要更灵活的内存管理策略。Box、Rc、Arc这三个智能指针覆盖了最常见的需求。

Box<T>:堆分配

Box<T>是最简单的智能指针,将数据分配到堆上,栈上只保留一个指针。

基本用法

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);  // 自动解引用,可以直接使用
}  // b离开作用域,堆上的数据自动释放

典型场景:递归类型

递归类型的大小在编译期无法确定,必须用Box间接引用:

// 这段代码无法编译——编译器不知道List有多大
// 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))))));
}

Deref Trait

Box实现了Deref trait,所以可以像普通引用一样使用:

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let s = Box::new(String::from("Rust"));
    hello(&s);  // Box<String> -> &String -> &str,自动解引用链
}

Rc<T>:引用计数

当一份数据需要多个所有者时,Rc<T>(Reference Counting)登场。它在运行时追踪引用计数,计数归零时释放数据。

限制:只能用在单线程环境。

use std::rc::Rc;

fn main() {
    let a = Rc::new(vec![1, 2, 3]);
    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
}

经典用例:共享图节点

use std::rc::Rc;

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

fn main() {
    let shared_tail = Rc::new(List::Cons(5, Rc::new(List::Cons(10, Rc::new(List::Nil)))));

    // a 和 b 共享同一个尾部
    let a = List::Cons(3, Rc::clone(&shared_tail));
    let b = List::Cons(4, Rc::clone(&shared_tail));
}

Weak<T>:打破循环引用

Rc的引用计数有个问题:如果两个Rc互相引用,计数永远不会归零,导致内存泄漏。Weak<T>不增加强引用计数,专门用来打破循环。

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

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的父节点(用弱引用)
    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

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

    println!("branch强引用: {}", Rc::strong_count(&branch)); // 1
    println!("branch弱引用: {}", Rc::weak_count(&branch));   // 1
}

规则很清晰:父->子用强引用(Rc),子->父用弱引用(Weak)

Arc<T>:线程安全版Rc

Arc(Atomically Reference Counted)和Rc功能一样,区别在于引用计数的增减使用原子操作,因此可以安全地跨线程共享。

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 || {
            let sum: i32 = data.iter().sum();
            println!("线程{}: sum = {}", i, sum);
        });
        handles.push(handle);
    }

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

Arc + Mutex:可变共享

Arc只提供共享的不可变访问。如果需要修改数据,配合Mutex

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
}

使用场景总结

智能指针 所有权 线程安全 可变性 典型场景
Box<T> 唯一 可Send 独占可变 递归类型、大数据堆分配、trait对象
Rc<T> 共享 不可变(配合RefCell) 单线程共享数据、图/树结构
Weak<T> - 打破Rc循环引用
Arc<T> 共享 不可变(配合Mutex) 多线程共享数据

选择原则:

  • 只需要堆分配 -> Box
  • 单线程多所有者 -> Rc(需要可变性加RefCell
  • 多线程多所有者 -> Arc(需要可变性加MutexRwLock
  • 有循环引用风险 -> 在合适的方向使用Weak