讲解

智能指针是「行为像指针、但带额外能力」的结构体。普通引用 &T 只是借用,智能指针通常拥有数据。标准库里三个最常用:Box、Rc、RefCell,分别解决三个不同的问题。

Box 最简单:把数据放到堆上,指针本身在栈上,唯一所有权。两个典型用途:一是递归类型(链表、树)——编译器需要知道类型大小,而递归嵌套大小无限,Box 的固定大小指针打破循环(enum List { Cons(i32, Box), Nil });二是大数据想避免栈上拷贝时。解引用用 *,和普通引用体验一致(Deref trait 的功劳)。

Rc(reference counted)提供同一数据多个所有者:Rc::clone 增加引用计数(注意是克隆计数不是克隆数据),计数归零才释放。只适合单线程(多线程用 Arc,第 24 章)。RefCell 则把借用规则从编译期挪到运行期检查:borrow()/borrow_mut() 拿到引用,规则违反时不是编译报错而是运行时 panic——这叫内部可变性(interior mutability),用于编译器分析不出、但你确知安全的场景。Rc<RefCell> 的组合=「多个所有者 + 可修改」,是图结构、回调共享状态的经典配方,代价是规则靠运行期兜底,写的时候要自己守住纪律。

示例

Box:堆分配与递归类型:

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

fn sum(list: &List) -> i32 {
    match list {
        List::Cons(value, next) => value + sum(next),
        List::Nil => 0,
    }
}

fn main() {
    let b = Box::new(5);
    assert_eq!(*b, 5); // 解引用和普通引用一样

    // 没有 Box,这个递归类型大小无限,编译不过
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
    assert_eq!(sum(&list), 3);
    println!("链表求和:{}", sum(&list));
}

Rc 共享所有权 + RefCell 内部可变性:

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

fn main() {
    // Rc:多个所有者共享一份数据
    let data = Rc::new(String::from("共享配置"));
    let reader1 = Rc::clone(&data);
    let reader2 = Rc::clone(&data);
    assert_eq!(Rc::strong_count(&data), 3);
    println!("{reader1} / {reader2}");

    // RefCell:运行期借用检查,编译期看不出的可变
    let log: RefCell<Vec<String>> = RefCell::new(Vec::new());
    log.borrow_mut().push(String::from("启动"));
    {
        let mut guard = log.borrow_mut();
        guard.push(String::from("就绪"));
    } // guard 在这里归还借用
    assert_eq!(log.borrow().len(), 2);
    println!("日志条数:{}", log.borrow().len());
}

常见坑

  • Rc 造成循环引用:A 持有 Rc、B 持有 Rc,计数永远到不了零——内存泄漏(是泄漏不是崩溃)。用 Weak 降级其中一个方向。
  • RefCell 重复可变借用:同一时刻两次 borrow_mut()(或 borrow 与 borrow_mut 重叠)直接 panic;借用守卫用完尽快离开作用域。
  • 把 Rc/RefCell 带进线程:两者都不是线程安全的,跨线程用编译报错——多线程版是 Arc 与 Mutex(下一章)。
  • 拿 Box 当共享指针用:Box 是唯一所有权;想共享用 Rc。三个指针各司其职,别混。

小结

Box 堆分配破递归;Rc 单线程共享所有权;RefCell 运行期借用检查;Rc<RefCell> 共享且可改。下一章把借用规则的价值推到顶点:并发。