Rust智能指针

发布于 作者: Ethan

一、智能指针的定义与设计动机

底层事实(First Principles)

  • 程序在运行过程中必然需要访问内存

  • 内存生命周期包含:分配、读写、释放

  • 任何阶段的失误都可能导致:

    • 悬垂指针(Dangling Pointer)
    • 内存泄漏(Memory Leak)
    • 数据竞争(Data Race)
    • 未定义行为(Undefined Behavior)

根本设计目标

在不存在垃圾回收(GC)的前提下,通过静态与动态约束,系统性地保证内存安全与并发安全

Rust 的核心创新在于: 将内存管理与并发约束显式编码进类型系统与所有权语义中

智能指针的本质

智能指针并非单纯的“地址抽象”,而是“所有权与访问规则的载体”

它们在类型层面精确描述:

  • 数据的所有权归属
  • 所有者数量
  • 生命周期结束条件
  • 是否允许可变访问
  • 是否具备跨线程共享能力

二、基础所有权模型:Box<T> ——堆分配的唯一所有权

问题抽象

需要将数据分配至堆内存,但仍然保持严格的唯一所有权语义

Box<T> 的语义特征

  • 在堆上分配数据
  • 唯一所有权(Single Ownership)
  • 生命周期与作用域严格绑定
  • 不引入任何额外的运行期开销
let b = Box::new(5);
// b 拥有堆上数据 5 的唯一所有权

典型使用场景

  1. 对象体积较大,不适合放置于栈
  2. 递归类型的合法化
enum List {
    Cons(i32, Box<List>),
    Nil,
}
  1. 显式表达“此对象必须位于堆上”

小结

Box<T> 本质上是 “将值的存储位置从栈迁移到堆,但不改变任何所有权与借用规则”


三、多所有权模型:Rc<T> ——单线程引用计数

问题升级

多个逻辑实体需要共享同一份不可变数据的所有权

Rust 默认的 Move 语义禁止此行为:

let a = String::from("hi");
let b = a; // 所有权发生转移

解决方案:引用计数(Reference Counting)

use std::rc::Rc;

let a = Rc::new(String::from("hi"));
let b = Rc::clone(&a);
let c = Rc::clone(&a);

Rc<T> 的语义约束

  • 允许多个所有者同时存在
  • 内部维护非原子引用计数
  • 引用计数归零时释放资源
  • 严格限定于单线程环境

不可跨线程的根本原因

  • 引用计数增减操作非原子
  • 并发修改会导致数据竞争
  • 无法满足 Rust 的线程安全保证(Send / Sync

小结

Rc<T> = “以运行期引用计数为代价,换取编译期所有权约束的放宽(仅限单线程)”


四、并发环境下的多所有权:Arc<T>

并发需求引入

多个线程需要安全地共享同一份数据的所有权

Arc<T> 的定义

Arc(Atomic Reference Counted)是 Rc 的并发安全版本:

use std::sync::Arc;

let a = Arc::new(5);
let b = Arc::clone(&a);

Rc<T>Arc<T> 对比

维度 Rc Arc
引用计数 非原子 原子
线程安全
性能开销 较低 略高

小结

Arc<T> = “通过原子操作保证引用计数一致性,从而实现跨线程的安全多所有权”


五、不可变性规则的根本冲突

Rust 的核心借用规则:

同一时间,要么存在任意数量的不可变借用,要么存在唯一的可变借用

然而在实际工程中,常见以下需求:

外部接口保持不可变,但内部状态需要按需更新

这直接催生了“内部可变性”模型。


六、运行期借用检查:RefCell<T> ——内部可变性

设计目标

当编译器无法在静态阶段证明借用安全,但程序员可以在逻辑上保证正确性

use std::cell::RefCell;

let x = RefCell::new(5);
*x.borrow_mut() += 1;

RefCell<T> 的工作机制

  • 编译期:跳过借用规则检查

  • 运行期

    • 任意时刻只能存在:

      • 一个可变借用
      • 或多个不可变借用
  • 违规行为直接触发 panic

常见组合模式

Rc<RefCell<T>>

语义表达:

单线程 + 多所有权 + 内部可变性

小结

RefCell<T> = “将借用规则从编译期推迟到运行期,由程序逻辑承担正确性责任”


七、线程安全的内部可变性:Mutex<T>RwLock<T>

并发进一步升级

多线程 + 多所有权 + 可变访问

Mutex<T>:互斥访问

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

let data = Arc::new(Mutex::new(5));

let mut guard = data.lock().unwrap();
*guard += 1;
  • 任意时刻仅允许一个线程访问
  • 基于阻塞同步

RwLock<T>:读写分离

  • 允许多个并发读
  • 写操作互斥
  • 适用于读多写少的场景

常见工程组合

Arc<Mutex<T>>
Arc<RwLock<T>>

小结

Mutex<T> / RwLock<T> = “通过同步原语,在时间维度上序列化对内存的访问”


八、弱引用模型:Weak<T>

引用计数的隐患

循环引用会导致引用计数永不归零,从而产生内存泄漏

Rc<A> → Rc<B>
Rc<B> → Rc<A>

Weak<T> 的语义定位

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

let weak: Weak<T> = Rc::downgrade(&rc);
  • 不参与所有权计数
  • 不延长生命周期
  • 使用前必须通过 upgrade() 显式检查有效性

小结

Weak<T> = “一种观察关系,而非所有权关系”


九、核心类型对照表

类型 所有权 可变性 线程安全 检查时机
Box<T> 唯一 编译期 编译期
Rc<T> 不可变 编译期
Arc<T> 不可变 编译期
RefCell<T> 唯一/多 可变 运行期
Mutex<T> 可变 运行期
RwLock<T> 可变 运行期
Weak<T> 不适用 同 Rc/Arc N/A

十、整体总结

Rust 的所有智能指针并非语法糖,而是类型化的内存访问契约。 它们的唯一目标是:

在编译期或运行期,以最小代价明确回答——

  • 谁可以访问内存
  • 何时可以访问
  • 以何种方式访问
  • 在什么并发条件下访问

内部可变性示例(附)

use std::cell::RefCell;

struct Fib {
    cache: RefCell<Vec<u64>>,
}

impl Fib {
    fn get(&self, n: usize) -> u64 {
        if let Some(v) = self.cache.borrow().get(n) {
            return *v;
        }

        let v = 42;

        // 借用规则在运行期检查
        self.cache.borrow_mut().push(v);
        v
    }
}