楼层: 首页/ 软件技术/ Rust 语言基础/ 智能指针:Box / Rc / Arc / RefCell
12

智能指针:Box / Rc / Arc / RefCell

Smart Pointers · Interior Mutability

智能指针是"比普通引用多一点元数据和能力"的类型,最典型就是 Box<T>、Rc<T>、Arc<T>、RefCell<T>。它们解决"所有权规则下不好表达"的场景。

Box<T>:把数据塞到堆上

// 递归类型大小未知,必须用 Box 包一层,否则编译器说"无限大小" enum List { Cons(i32, Box<List>), Nil, } use List::{Cons, Nil}; fn main() { let b = Box::new(5); // 5 在堆上,b 在栈上存指针 println!("{}", b); // *b 解引用,自动 deref coercion let list = Cons(1, Box::new(Cons(2, Box::new(Nil)))); println!("链表建好了"); }

什么时候用 Box:递归类型、大小编译期未知的类型、想转移大数据所有权而不拷贝、想存 trait 对象(Box<dyn Trait>)。

Rc<T> 与 Arc<T>:多所有者共享

use std::rc::Rc; fn main() { // Rc:引用计数,单线程多个所有者共享同一份数据 let a = Rc::new(String::from("共享")); let b = Rc::clone(&a); // 只增加引用计数,不拷贝数据 println!("强引用数 {}", Rc::strong_count(&a)); // 2 println!("{} 与 {}", a, b); // 多线程场景把 Rc 换成 Arc(原子引用计数,线程安全) }
$ cargo run 强引用数 2 共享 与 共享
Rc vs Arc:单线程 vs 多线程
Rc<T>Arc<T>
线程安全不安全(不是 Send)安全(原子计数)
开销略小略大(原子操作)
用途单线程多所有者跨线程多所有者,std::sync::Arc

RefCell<T>:编译期借不过,运行时借

论内部可变性模式

正常情况借用规则编译期查。但有些场景你"逻辑上"确定没冲突,编译期却查不出来——这时候用 RefCell<T>:借用检查推迟到运行时。它 .borrow() 读、.borrow_mut() 写,真冲突了运行时 panic(不编译错)。单线程用 RefCell;Cell<T> 给 Copy 类型用 get/set。

use std::cell::RefCell; fn main() { let cell = RefCell::new(5); *cell.borrow_mut() = 10; // 运行时可变借用 println!("{}", cell.borrow()); // 运行时不可变借用 }
$ cargo run 10

黄金组合:多所有者可变共享

按场景选组合
需求用这个组合
单线程,多人共有且可变Rc<RefCell<T>>
多线程,多人共有且可变Arc<Mutex<T>>(下一章)
只想要共享不可变Rc<T> / Arc<T>
循环引用与 Weak

Rc 互相引用会形成环,计数永远归不了零,内存泄漏。解法是用 Weak<T>(弱引用,不增加计数,.upgrade() 返回 Option<Rc<T>>)。常见在"父节点持有子节点强引用,子节点持有父节点弱引用"的树结构里。

另外两个底层:Cow<'a, B> 是写时复制——读零成本,真要写时才克隆,适合"大多只读偶尔改写";Deref 和 Drop 两个 trait 分别管 * 解引用和析构,是自定义智能指针的基础。

Cell<T>:Copy 类型的轻量内部可变性

use std::cell::Cell; fn main() { // Cell 适合 i32/bool 这种 Copy 类型:不拿引用,直接整体替换 let c = Cell::new(5); c.set(10); // 即使 c 是不可变绑定,也能改内部值 println!("{}", c.get()); // 10,整体拷出来 }
$ cargo run 10
Cell vs RefCell:怎么选
Cell<T>RefCell<T>
适用类型只适合 T: Copy(i32/bool 等)任意 T
改值方式.set() 整体替换,不发引用.borrow_mut() 拿可变引用
运行时开销更小(不做借用计数)有借用计数,写错 panic
典型用途计数器、缓存标志位需要持有复杂类型的可变引用