先搞清楚一件事

Rust 的”智能指针”概念和 C++ 完全是两码事。在 C++ 里,智能指针是用来擦屁股的——程序员管不好内存,标准库帮你想办法。Rust 不一样,所有权系统在编译期就解决了 99% 的内存安全问题,所谓的智能指针只是所有权规则的延伸。

记住这句话:Rust 的智能指针不是为了修复烂代码,而是为了实现特定的数据结构需求。

Box:最简单的堆分配

Box<T> 就是 Rust 版的 std::unique_ptr,但更简单、更严格。

fn main() {
    // 在堆上分配一个 i32
    let b = Box::new(5);
    println!("{}", b); // 自动解引用

    // Box 离开作用域,内存自动释放
} // drop 在这里调用

什么时候用 Box?

  1. 递归类型必须用它(这是编译器逼你的):
struct ListNode {
    val: i32,
    next: Option<Box<ListNode>>
}
  1. 数据太大,不想栈上拷贝:
fn process(data: Box<[u8; 1024 * 1024]>) {
    // 转移所有权,零拷贝
}
  1. Trait Object:
fn factory() -> Box<dyn Draw> {
    Box::new(Circle::new())
}

Rc:单线程共享所有权

Rc<T>(Reference Counted)是单线程的共享指针。注意:它只适用于单线程

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    
    // clone 只增加引用计数,不拷贝数据
    let data2 = Rc::clone(&data);
    let data3 = Rc::clone(&data);
    
    println!("引用计数: {}", Rc::strong_count(&data)); // 3
    
    // 所有 Rc 都离开作用域,数据才真正释放
}

核心特点:

  • 不可变借用:你只能读,不能写
  • 非线程安全:没有 SendSync trait
  • 有循环引用风险(需要用 Weak<T> 破解)
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,     // Weak 打破循环
    children: RefCell<Vec<Rc<Node>>>, // Rc 拥有子节点
}

Arc:线程安全的共享

Arc<T>(Atomic Reference Counted)是线程安全版的 Rc<T>。内部用原子操作维护引用计数,有性能开销。

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

fn main() {
    let data = Arc::new(vec![1, 2, 3]);
    
    let handles: Vec<_> = (0..10)
        .map(|i| {
            let data = Arc::clone(&data);
            thread::spawn(move || {
                println!("线程 {}: {:?}", i, data);
            })
        })
        .collect();
    
    for h in handles {
        h.join().unwrap();
    }
}

Arc 的坑:

Arc 给你的是共享所有权,但如果你需要修改数据,还得配合 Mutex<T>RwLock<T>

use std::sync::{Arc, Mutex};

fn main() {
    let counter = Arc::new(Mutex::new(0));
    
    let handles: Vec<_> = (0..10)
        .map(|_| {
            let counter = Arc::clone(&counter);
            thread::spawn(move || {
                let mut num = counter.lock().unwrap();
                *num += 1;
            })
        })
        .collect();
    
    for h in handles {
        h.join().unwrap();
    }
    
    println!("结果: {}", *counter.lock().unwrap());
}

RefCell:运行期借用检查

RefCell<T> 实现了内部可变性(Interior Mutability)——让你能在不可变引用里修改数据。

原理:

  • 普通借用检查在编译期进行
  • RefCell 把检查推迟到运行期
  • 违反规则会panic,而不是编译错误
use std::cell::RefCell;

fn main() {
    let cell = RefCell::new(5);
    
    {
        let mut v = cell.borrow_mut();  // 可变借用
        *v += 1;
    } // 借用在这里结束
    
    let v = cell.borrow();  // 不可变借用
    println!("{}", v);  // 6
}

双借用错误(运行期):

let cell = RefCell::new(5);
let _a = cell.borrow_mut();
let _b = cell.borrow_mut(); // thread 'main' panicked at already borrowed

Rc<RefCell> 组合:

这是单线程下共享可变数据的经典套路:

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let shared = Rc::new(RefCell::new(Vec::new()));
    
    let s1 = Rc::clone(&shared);
    let s2 = Rc::clone(&shared);
    
    s1.borrow_mut().push(1);
    s2.borrow_mut().push(2);
    
    println!("{:?}", shared.borrow()); // [1, 2]
}

Cell:更轻量的内部可变性

Cell<T> 也是内部可变性,但比 RefCell 更轻量。它只适用于实现了 Copy trait 的类型。

use std::cell::Cell;

fn main() {
    let cell = Cell::new(5);
    
    cell.set(10);           // 直接设置新值
    let old = cell.replace(20);  // 替换并返回旧值
    
    println!("old: {}, new: {}", old, cell.get()); // old: 10, new: 20
}

Cell vs RefCell:

特性CellRefCell
适用类型Copy 类型任意类型
借用检查无(整体替换)运行期检查
开销极低有一定开销
获取引用不能borrow() / borrow_mut()

与 C++ 智能指针的对比

特性RustC++
所有权检查编译期强制运行期/程序员自律
默认行为移动语义拷贝语义
Box<T> / unique_ptr编译期 guarantee运行期可能 nullptr
Rc<T> / shared_ptr明确单线程限制一律线程安全(有开销)
循环引用Weak 明确标记程序员自己注意
性能零成本抽象虚函数/控制块开销

Linus 的评价:

“Rust 这帮家伙做对了一件事——让编译器当坏人

C++ 的智能指针是亡羊补牢,Rust 的所有权系统是防患于未然。
我讨厌 C++ 的复杂性,但 Rust 的编译错误虽然烦人,至少问题在编译期就暴露了。”

选择指南:实际场景

// 场景 1:树结构,子节点被父节点拥有
struct TreeNode {
    value: i32,
    children: Vec<Rc<RefCell<TreeNode>>>,
    parent: Option<Weak<RefCell<TreeNode>>>,
}

// 场景 2:多线程共享配置
lazy_static! {
    static ref CONFIG: Arc<RwLock<Config>> = Arc::new(RwLock::new(Config::default()));
}

// 场景 3:回调函数闭包捕获
let data = Rc::new(RefCell::new(Vec::new()));
let closure = {
    let data = Rc::clone(&data);
    move || {
        data.borrow_mut().push(42);
    }
};

// 场景 4:大型数据避免栈拷贝
fn process_big_data(data: Box<[u8; 1024 * 1024]>) {
    // 数据已经在堆上,转移所有权即可
}

常见的混合使用

实际项目中,智能指针经常组合使用:

// Rc<RefCell<T>> - 单线程共享可变数据
let state = Rc::new(RefCell::new(GameState::new()));
state.borrow_mut().score += 100;

// Arc<Mutex<T>> - 多线程共享可变数据  
let counter = Arc::new(Mutex::new(0));
*counter.lock().unwrap() += 1;

// Arc<RwLock<T>> - 多读单写场景
let config = Arc::new(RwLock::new(Config::default()));
let cfg = config.read().unwrap();  // 多线程可同时读
let mut cfg = config.write().unwrap(); // 独占写

// Rc<Cell<T>> - 轻量级计数器
let count = Rc::new(Cell::new(0));
count.set(count.get() + 1);

// Rc<RefCell<Vec<T>>> - 共享可变集合
let callbacks: Rc<RefCell<Vec<Box<dyn Fn()>>>> = 
    Rc::new(RefCell::new(Vec::new()));
callbacks.borrow_mut().push(Box::new(|| println!("callback")));

选择建议:

场景组合注意点
单线程 + 共享 + 可变Rc<RefCell<T>>运行期检查,可能 panic
单线程 + Copy类型可变Rc<Cell<T>>无借用检查,更轻量
多线程 + 共享 + 可变Arc<Mutex<T>>锁粒度要小,避免死锁
多读少写Arc<RwLock<T>>注意写锁饥饿问题
递归数据结构Box + Rc + Weak用 Weak 打破循环引用

关键原则

  1. 默认用 ownership + borrowing,别一上来就用智能指针
  2. Box:递归类型、大对象、Trait Object
  3. Rc:单线程共享只读数据(配合 RefCell 可写)
  4. Arc:多线程共享只读数据(配合 Mutex/RwLock 可写)
  5. Cell:Copy 类型的内部可变性
  6. RefCell:非 Copy 类型的内部可变性(单线程)

记住: Rust 的智能指针是为了表达特定的所有权语义,不是为了修复你的设计缺陷。如果你在纠结用哪个指针,先想想你的所有权模型设计是否正确
2. Box:递归类型、大对象、Trait Object
3. Rc:单线程共享只读数据(配合 RefCell 可写)
4. Arc:多线程共享只读数据(配合 Mutex/RwLock 可写)
5. Cell:Copy 类型的内部可变性
6. RefCell:非 Copy 类型的内部可变性(单线程)