28-智能指针
15 分钟阅读
智能指针 (Smart Pointers)
面向 Rust 1.97.1 (stable, 2026-07)。这篇不把智能指针讲成“更多星号”,而是把它们放回所有权、共享和内部可变性的语境里。
本篇能解决什么:
- 你是否总把
Box、Rc、Arc、RefCell、Mutex混成“都是某种指针”? - 你是否想搞懂为什么
Rc<RefCell<T>>、Arc<Mutex<T>>会成为固定搭配? - 你是否不清楚
Weak、Cow、Pin解决的到底是哪类具体问题? - 你是否需要一个按“所有权 / 共享 / 可变性 / 线程”拆分的记忆方式?
术语速查表:
| 术语 / 缩略词 | 全称 / 读法 | 中文 | 一句话解释 | Go 里的近亲 |
|---|---|---|---|---|
Box<T> | — | 盒指针 | 最简单的拥有型堆指针 | 堆上对象 |
Rc<T> | Reference Counted | 引用计数指针 | 单线程共享所有权 | GC 管理对象的部分感觉 |
Arc<T> | Atomically Reference Counted | 原子引用计数指针 | 多线程共享所有权 | 线程安全共享对象 |
RefCell<T> | — | 运行时借用容器 | 把借用检查推迟到运行时 | 单线程可变容器 |
Weak<T> | — | 弱引用 | 不增加强引用计数,常用来打破环 | 弱引用 |
Pin | — | 固定位置 | 保证值在被 pin 后不再被安全移动 | Go 无直接对应物 |
热度索引:
| 热度 | 题目 |
|---|---|
common | Q1, Q2, Q3, Q4, Q5, Q6, Q7, Q8, Q9, Q10, Q11, Q12, Q13, Q14, Q15, Q16, Q17, Q18, Q19, Q20 |
Q1. 智能指针和普通引用最本质的区别是什么?
Tags: common smart-pointer ownership
适用版本: Rust 1.0+
一句话答案: 普通引用只是借用视图;智能指针通常还携带所有权、引用计数、运行时借用检查或析构逻辑。
详细解答: 把智能指针理解成“带行为的数据结构”更准确。它们不只是某个地址,而是围绕地址建立的一套规则。
| |
| |
🐹 Go 对比: Go 指针主要表达“可间接访问”;Rust 智能指针更常在表达“谁拥有、能否共享、怎样释放”。
记忆点: 先问它管理的是什么规则,再问它像不像指针。
Q2. Box<T> 最常见的两个用途是什么?
Tags: common Box
适用版本: Rust 1.0+
一句话答案: 最常见的是把值放到堆上,以及为递归类型提供一个固定大小的间接层。
详细解答: Box<T> 是学习智能指针的起点,因为它只解决“单一所有者 + 堆分配”这件事,语义最简单。
| |
| |
🐹 Go 对比: Go 让编译器决定是否逃逸到堆;Rust 这里是显式选择堆分配。
记忆点: Box 解决“单所有者的堆上值”,不解决共享和可变借用冲突。
Q3. Rc<T> 和 Arc<T> 该怎么区分?
Tags: common Rc Arc
适用版本: Rust 1.0+
一句话答案: 二者都是共享所有权;Rc 只给单线程用,Arc 才能安全跨线程。
详细解答: 区别不在 API 形状,而在引用计数是否原子化。多线程需要原子计数,因此 Arc 会有额外开销。
| |
| |
🐹 Go 对比: Go 默认所有堆对象都能在多 goroutine 间共享,但正确同步要靠你自己;Rust 先从类型上把 Rc 和 Arc 分开。
记忆点: 单线程优先 Rc,跨线程才上 Arc。
Q4. 为什么 Weak<T> 是图和树结构里的高频工具?
Tags: common Weak
适用版本: Rust 1.0+
一句话答案: 因为 Rc / Arc 的强引用环会泄漏,Weak 不增加强计数,能安全表达“我知道你可能存在,但我不拥有你”。
详细解答: 树结构里最典型的用法是:父节点强持有子节点,子节点弱引用父节点。这样父一释放,子里的父指针就自然失效成 None。
| |
| |
🐹 Go 对比: Go 有 GC,因此环不会变成传统意义的内存泄漏;Rust 的引用计数不是 tracing GC,所以必须显式打破环。
记忆点: “反向边常用 Weak” 是很实用的工程口诀。
Q5. Cell<T> 和 RefCell<T> 的区别该怎么记?
Tags: common Cell RefCell
适用版本: Rust 1.0+
一句话答案: Cell<T> 适合整值替换,RefCell<T> 适合借出引用;两者都属于单线程内部可变性工具。
详细解答: 内部可变性指的是“即使外部只有 &self,内部仍然能改”。代价是:RefCell 把部分借用检查延后到运行时。
| |
| |
🐹 Go 对比: Go 不需要专门的“内部可变性容器”,因为默认共享可变状态本就能随时修改;Rust 则要求你显式选择这条路。
记忆点: Cell 改整个值,RefCell 借出后再改内容。
Q6. RefCell<T> 为什么会 panic,它到底换来了什么?
Tags: common RefCell runtime-check
适用版本: Rust 1.0+
一句话答案: 因为它把“同一时刻要么多读要么一写”的借用规则挪到了运行时检查,违规时就会 panic。
详细解答: 这不是弱化规则,而是换检查时机。你得到的是更灵活的单线程共享可变结构,付出的代价是可能在运行时炸掉。
| |
| |
🐹 Go 对比: Go 常靠约定和测试保证“这里不会乱改”;Rust 允许你显式把这部分约束变成运行时检查。
记忆点: RefCell 不是“更自由”,而是“规则不变、检查延后”。
Q7. 为什么 Rc<RefCell<T>> 经常一起出现?
Tags: common Rc RefCell
适用版本: Rust 1.0+
一句话答案: 因为 Rc 解决共享所有权,RefCell 解决共享后的可变访问,两者刚好互补。
详细解答: 这通常用于单线程图结构、UI 树、观察者模式等需要“很多地方都能拿到同一份数据,而且有人要改”的场景。
| |
| |
🐹 Go 对比: Go 里常直接共享一个指针并修改;Rust 需要你把“共享”和“可变”这两层显式拼起来。
记忆点: 单线程共享可变状态,优先想到 Rc<RefCell<T>>。
Q8. 多线程里为什么又变成 Arc<Mutex<T>>?
Tags: common Arc Mutex
适用版本: Rust 1.0+
一句话答案: 因为跨线程共享需要 Arc,跨线程可变访问需要锁;Mutex<T> 是最基本的一把互斥锁。
详细解答: 这和 Rc<RefCell<T>> 的结构几乎平行,只是把单线程运行时借用检查换成了线程安全锁。
| |
| |
🐹 Go 对比: 这最像 sync.Mutex + 共享对象;不同点在于 Rust 会先从类型层面禁止你把 Rc<RefCell<T>> 直接搬到线程里。
记忆点: 多线程共享可变状态,先考虑 Arc<Mutex<T>>。
Q9. Deref 为什么能让 Box<String> 看起来像 String 一样用?
Tags: common Deref
适用版本: Rust 1.0+
一句话答案: 因为智能指针实现了 Deref 后,编译器会在合适场景自动做解引用和解引用强制转换。
详细解答: 这也是为什么很多智能指针“用起来不像指针”:方法调用和参数传递时,编译器经常会帮你沿着 Deref 链走到目标类型。
| |
| |
🐹 Go 对比: Go 没有这套 trait 驱动的自动解引用链;Rust 把它建立在 Deref 抽象之上。
记忆点: “像原值一样用” 往往来自 Deref,不是语法特殊照顾。
Q10. Drop 的实际意义是什么,为什么不能手动调用 Drop::drop?
Tags: common Drop
适用版本: Rust 1.0+
一句话答案: Drop 用来在值离开作用域时自动清理资源;你可以提前 drop(value),但不能直接手动调析构方法本身。
详细解答: Rust 要保证析构只发生一次,所以不允许你随便直接调用 Drop::drop。标准做法是让作用域结束,或调用 std::mem::drop 提前释放。
| |
| |
🐹 Go 对比: Go 更依赖 GC 和 defer;Rust 的 Drop 更接近“离开作用域自动析构”。
记忆点: 想提前释放就用 drop(value),不要碰 Drop::drop。
Q11. Cow<'a, T> 真正适合哪类场景?
Tags: common Cow
适用版本: Rust 1.0+
一句话答案: 当“多数情况下只借用、少数情况下才需要拷贝并修改”时,Cow 非常合适。
详细解答: Cow 是 Clone-on-Write 的缩写。它避免了“永远分配新 String”和“永远只能借用”这两种极端。
| |
| |
🐹 Go 对比: Go 字符串不可变,很多 API 会直接返回新字符串;Rust 的 Cow 则能把“是否复制”延后到真正需要修改时。
记忆点: “大多数借用,少数拥有” 是 Cow 的主场。
Q12. Pin 为什么常跟 async 和自引用类型一起出现?
Tags: common Pin
适用版本: Pin 自 Rust 1.33+
一句话答案: 因为某些值一旦内部形成“指向自身一部分”的关系,就不能再被安全移动,而 Pin 正是在表达这种限制。
详细解答: 应用代码不一定天天手写 Pin,但只要碰到自定义 future、异步底层实现或自引用抽象,迟早会见到它。
| |
| |
🐹 Go 对比: Go 不暴露这类“值是否还能移动”的类型级约束;Rust 为了支持自引用状态机,把这层细节显式化了。
记忆点: 看到 Pin,先想到“某些移动会破坏内部不变式”。
Q13. 什么时候用 Box,什么时候 Rc/Arc?
Tags: common Box Rc Arc
适用版本: Rust 1.0+
一句话答案: 只有一个所有者时用 Box;单线程多所有者用 Rc;要跨线程共享再用 Arc。
解答: 选型先问两件事:要不要共享所有权,以及要不要跨线程。Box 最简单——独占堆上值,适合递归类型、大对象上堆、Box<dyn Trait>。一旦多个地方都要“拥有同一份”,才升级到引用计数;多线程再换成原子版 Arc(见 Q3)。
| |
| |
Go 对比:
| |
- Go 怎么做:堆对象天然可多指针共享,生命周期靠 GC。
- Rust 为什么不同:
Box/Rc/Arc把“独占 / 单线程共享 / 跨线程共享”写进类型。 - Go 程序员易踩的坑:一上来就
Rc/Arc,其实很多地方Box或普通拥有值就够。
记忆点:
- 独占 →
Box;同线程共享 →Rc;跨线程 →Arc。 - 先问“几个所有者”,再问“几个线程”。
Q14. Rc 成环会泄漏吗?Weak 怎么解?
Tags: common Rc Weak leak
适用版本: Rust 1.0+
一句话答案: 会:强引用环让强计数永远到不了 0,内存不会释放;把反向边改成 Weak 就能打破环。
解答: Rc/Arc 不是 tracing GC,只靠强引用计数。A 强持有 B、B 又强持有 A 时,双方计数都 ≥1,作用域结束也清不掉。树/图里常见做法:父→子用 Rc,子→父用 Weak(见 Q4)。upgrade() 得到 Option<Rc<_>>:父还在则 Some,已释放则 None。
| |
| |
Go 对比:
| |
- Go 怎么做:环对 GC 通常不是泄漏;对象不可达才会收。
- Rust 为什么不同:引用计数环会真泄漏,必须显式
Weak。 - Go 程序员易踩的坑:以为“有 GC 的语言才有环问题”,反而在 Rust 里忽略环。
记忆点:
- 强环 = 泄漏;反向边优先
Weak。 upgrade失败说明强所有者已经没了。
Q15. RefCell 和 Mutex 怎么区分(单线程内部可变 vs 多线程)?
Tags: common RefCell Mutex
适用版本: Rust 1.0+
一句话答案: RefCell 是单线程运行时借用检查;Mutex 是多线程互斥锁。固定搭配常是 Rc<RefCell<T>> 与 Arc<Mutex<T>>。
解答: 两者都解决“外部不可变、内部要改”,但线程模型不同。RefCell 违规会 panic,不能跨线程;Mutex 用锁串行化访问,可与 Arc 一起跨线程(见 Q7、Q8)。单线程图结构别无端上锁;多线程别拿 RefCell 硬闯。
| |
| |
Go 对比:
| |
- Go 怎么做:共享可变几乎总靠锁或 channel;没有
RefCell这种单线程专用工具。 - Rust 为什么不同:先按线程边界选型,避免把锁开销带进单线程代码。
- Go 程序员易踩的坑:凡是“要改共享状态”就直接
Mutex,忽略单线程场景的RefCell。
记忆点:
- 单线程内部可变 →
RefCell;多线程 →Mutex。 - 口诀:
Rc+RefCell,Arc+Mutex。
Q16. Go 里的指针/堆对象,在 Rust 常映射成什么?
Tags: common go-compare Box Rc Arc
适用版本: Rust 1.0+
一句话答案: Go 的 *T/堆对象常对应 Rust 的拥有值、Box、或显式共享的 Rc/Arc(再加可变包装);没有“默认多别名 + GC”的单一映射。
解答: Go 指针拷贝只是多一个别名;Rust 赋值经常是 move。堆上独占 → Box;只读共享可多 Rc/Arc;还要改则叠 RefCell/Mutex。很多时候甚至不必指针:String、Vec、结构体本身已是拥有型值。先问所有权与可变性,再选智能指针。
| |
| |
Go 对比:
| |
- Go 怎么做:
new/&得到可共享堆对象,GC 回收。 - Rust 为什么不同:共享与可变都要显式选型,编译器拦不安全别名。
- Go 程序员易踩的坑:把每个
*T都翻译成Box,或反过来到处&却忘了生命周期。
记忆点:
- Go
*T≠ 固定等于 Rust 某一种指针。 - 映射轴是:独占 / 共享 / 可变 / 是否跨线程。
Q17. Deref / DerefMut 和方法解析到底怎么配合?
Tags: common Deref DerefMut method-resolution
适用版本: Rust 1.0+
一句话答案: 方法调用时,编译器会沿 Deref(可变场景再沿 DerefMut)自动解引用,直到找到匹配方法;赋值、字段访问等场景不会无限制地帮你“像原类型一样处处等价”。
解答: Deref(解引用 trait)让智能指针在方法调用、部分参数强制转换时表现得像内部类型;DerefMut 则对应可变路径。Box/Rc/String/Vec 都靠这套机制。注意:方法解析会自动解引用,但 * 解引用、模式匹配、部分运算符重载仍要你自己写清楚。自写类型实现 Deref 时,目标应是“智能指针语义”,不要拿它当随意的隐式转换工具。
| |
| |
Go 对比:
| |
- Go 怎么做:方法集在
T与*T之间有固定规则,没有Deref链。 - Rust 为什么不同:用 trait 表达“指针样类型如何露出内部值”。
- Go 程序员易踩的坑:以为实现了
Deref就处处等于内部类型;赋值与所有权规则仍按外层类型走。
记忆点:
- 方法调用常自动
Deref;可变方法走DerefMut。 Deref是智能指针协议,不是通用隐式转换。
Q18. Rc::make_mut / Arc::make_mut 的写时克隆是什么?
Tags: common Rc Arc make_mut
适用版本: Rust 1.0+(Rc/Arc);make_mut 为标准库长期稳定 API
一句话答案: make_mut 在强引用计数为 1 时直接给出可变借用;若还有其它强引用,则先克隆一份再可变——典型的写时克隆(clone-on-write)。
解答: 适合“多数只读共享,偶尔要改且改完只影响自己这份”的数据。它不会动弱引用计数逻辑以外的魔法:有别人还强持有同一块时,你改的是克隆后的新分配。需要“改一处、所有别名一起变”时,应改用 RefCell/Mutex 等内部可变性,而不是 make_mut。
| |
| |
Go 对比:
| |
- Go 怎么做:共享切片/map 时靠约定拷贝;没有内建
make_mut。 - Rust 为什么不同:引用计数指针能在“唯一所有者”时原地改,多所有者时自动拆分。
- Go 程序员易踩的坑:以为
make_mut会就地改所有Rc别名;有克隆时只改自己那份。
记忆点:
- 唯一强引用 → 原地可变;否则先克隆。
- 要共享可变状态,别用
make_mut冒充。
Q19. 什么时候别急着上 Rc / Arc?
Tags: common Rc Arc api-design
适用版本: Rust 1.0+
一句话答案: 单一所有者、能 move、能按值返回、或短期借用就够时,先用拥有值 / Box / 普通引用;只有真正出现“多个所有者同时活着”再上引用计数。
解答: 过早 Rc 会带来:计数开销、更难推理的生命周期、成环泄漏风险、以及“到处 clone 指针”的 API 味道。优先顺序常见是:拥有并转移 → 借用 → Box 独占堆 → 最后才 Rc/Arc(见 Q13)。树若只有父拥有子,子不必反向强持有父;需要回调时也可以先传 &T/&mut T。
| |
| |
Go 对比:
| |
- Go 怎么做:切片/指针共享很廉价,GC 托底,较少“先证明唯一所有者”。
- Rust 为什么不同:默认所有权更简单可证明;共享是显式升级。
- Go 程序员易踩的坑:把每个“多处能看见”都翻译成
Rc,其实借用或 move 就够。
记忆点:
- 先问“能不能唯一拥有或只借一会儿”。
Rc/Arc是共享所有权工具,不是默认指针。
Q20. Box::leak、into_raw / from_raw 有哪些坑?
Tags: common Box leak raw-pointer
适用版本: Rust 1.0+
一句话答案: Box::leak 故意永不回收;into_raw 交出所有权后必须且只能用 from_raw 接回一次——重复释放、忘记接回、或接回后继续用旧裸指针,都会变成内存错误。
解答: Box::leak 适合真正要 'static 且接受泄漏的场景(插件表、进程级缓存);日常业务数据别用它“逃生命周期”。into_raw 把 Box 变成 *mut T 后,Rust 不再自动 drop;你要自己保证:要么 from_raw 重建 Box 再释放,要么交给明确约定的 C/FFI 释放方。切勿对同一地址 from_raw 两次,也别在 from_raw 之后继续解引用旧指针。
| |
| |
Go 对比:
| |
- Go 怎么做:GC 管回收;逃逸分析可能把局部放到堆上。
- Rust 为什么不同:
Box的释放责任明确;泄漏与裸指针交接必须显式。 - Go 程序员易踩的坑:用
leak假装解决了生命周期;或把into_raw当“随便拿个指针”却忘了配对释放。
记忆点:
leak= 故意不释放;先确认真要'static。into_raw/from_raw必须一对一,禁止双释放。