Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

智能指针

Rust 里“智能指针”这块,按 “它解决什么问题 + 它的所有权/借用规则 + 运行时成本” 来系统学。下面把需要掌握的知识点尽量全列出来,并按学习路径组织。


定义

什么算“智能指针”

  • 智能指针 vs 普通引用 &T / &mut T :智能指针是“像指针一样用(Deref)+ 还带管理/策略”的类型
  • 三个核心 trait:
    • Deref / DerefMut:让 *p、方法调用、解引用自动转换成立
    • Drop:离开作用域时自动清理资源(RAII)
    • CoerceUnsized/DispatchFromDyn(了解即可):Box<T>Box<dyn Trait> 的不定大小转换等

要理解为什么 BoxString 是智能指针,必须先看它们背后的两个关键特征。智能指针本质上是一个实现了 DerefDrop 的结构体

A. Deref 特征:让结构体“像”指针

普通的结构体不能被解引用(即不能用 *)。实现了 Deref 之后,智能指针就可以像普通引用一样工作。

  • 解引用强制转换 (Deref Coercion):这是 Rust 的魔法。如果一个函数需要 &str 参数,你传给它 &StringString实现了 Deref),Rust会自动帮你转换。
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}
// 实现 Deref 告诉编译器:当我对 MyBox 使用 * 时,返回内部的那个值
impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.0
    }
}
fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y); // 这里的 *y 实际上执行的是 *(y.deref())
    println!("{}", *y); 
}

B. Drop 特征:自动清理的“析构函数”

Drop 特征允许你自定义:当一个变量离开作用域时该发生什么。对于智能指针,这通常意味着释放它所拥有的堆内存

  • 自动调用:你不需要手动调用 drop,Rust 会自动在变量生命周期结束时插入清理代码。
  • 防止泄漏:有了 Drop,Rust 确保了即便程序在中间出错退出,堆内存也会被正确回收。
struct CustomSmartPointer {
    data: String,
}
impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("正在清理数据: {}!", self.data);
    }
}
fn main() {
    let c = CustomSmartPointer { data: String::from("我的数据") };
    let d = CustomSmartPointer { data: String::from("其他数据") };
    println!("智能指针已创建。");
    // 函数结束时,d 先被清理,c 后被清理(先进后出)
}

智能指针通过 Deref 让我们能方便地访问数据,通过 Drop 让我们不再担心内存回收。


1. Box<T>:堆分配 & 递归类型

你要会的点:

  • 什么时候需要 Box
    • 把值放到上(存储实际的数据 T),而在(存储指针地址,64 位系统通常是8字节)上仅保留一个指向堆数据的指针
    • 处理 递归类型 (编译期需要知道每个类型占用多少内存空间–如:链表)
    • 作为 trait object 的持有者:当你需要一个“实现了某个特征的类型”而不是具体类型时,通常使用Box<dyn Trait>
    • 转移大数据所有权,如果你有一个非常大的结构体或数组,将其作为参数传递给函数时,如果不使用指针,Rust默认会进行栈拷贝,这非常消耗性能。
  • 所有权语义Box<T> 独占所有权,同一时间只能有一个 Box 指向该堆数据,移动后原变量不可用
  • 解引用与方法调用Box<T> 自动 DerefT

示例:递归类型(链表)

enum List {
    Cons(i32, List), // 错误:List 包含 List,会导致无限大小
    Nil,
}

编译器会报错,因为 Cons 变体包含另一个 List,这会形成无限递归,编译器无法计算 List 结构的大小。解决方案: 使用Box 智能指针,将递归类型“在堆上间接存储”,List本身就有固定大小了。

// 递归 enum:如果没有 Box,编译器无法在编译期知道 List 的大小。
// Box<List> 让递归“在堆上间接存储”,List 本身就有固定大小了。
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),//Cons 占用:一个 i32 的空间 + 一个指针的空间
    Nil,
}
impl List {
    fn new() -> Self {
        List::Nil
    }
    fn prepend(self, v: i32) -> Self {
        List::Cons(v, Box::new(self))
    }
    fn len(&self) -> usize {
        match self {
            List::Cons(_, next) => 1 + next.len(),
            List::Nil => 0,
        }
    }
}
fn main() {
    let list = List::new().prepend(3).prepend(2).prepend(1);
    println!("{list:?}");
    println!("len = {}", list.len());
}

示例:返回 trait object(动态分发)

trait Draw {
    fn draw(&self);
}
struct Circle {r: f32}
impl Draw for Circle {
    fn draw(&self) {
        println!("Circle radius = {}", self.r);
    }
}
struct Square {side: f32}
impl Draw for Square {
    fn draw(&self) {
        println!("Square side = {}", self.side);
    }
}
// 返回 Box<dyn Draw>:调用者只知道“它能 draw”,不关心具体类型。
// 这会使用动态分发(vtable)。
fn make_shape(kind: &str) -> Box<dyn Draw> {
    match kind {
        "circle" => Box::new(Circle { r: 2.0 }),
        _ => Box::new(Square { side: 3.0 }),
    }
}
fn main() {
    let s1 = make_shape("circle");
    let s2 = make_shape("square");
    s1.draw();
    s2.draw();
}

2. Rc<T>:单线程引用计数共享所有权

Rust 的所有权规则中,通常一个值只能有一个所有者。但在某些复杂的应用场景中(例如图结构、社交网络或共享配置),一个数据可能需要被多个不同的部分共同拥有。这时,Box<T> 的独占所有权就不够用了,我们需要 Rc<T>(Reference Counted引用计数)。

你要会的点:

  • 为什么需要 Rc :它允许一个数据拥有多个所有者。它在堆上存储数据,并额外记录一个“引用计数器”,用来统计目前有多少个指针指向这份数据
  • 只读共享Rc<T> 默认只允许你不可变地借用数据。如果你想通过其中一个 Rc 修改数据,编译器会拒绝(除非配合后面要讲的 RefCell)。
  • 共享而非拷贝:当你“克隆”一个 Rc<T> 时,Rust 并不会在堆上重新分配内存并拷贝数据,而是
    • 增加引用计数:计数器加 1。
    • 拷贝指针地址:在栈上创建一个新的指针指向同一个堆位置。
    • 只有当计数器归零时: 堆上的数据才会被真正清理。
  • 强引用/弱引用
    • Rc::clone(&rc) 增加强引用计数(不是深拷贝)
    • Weak<T> 用于打破环,避免内存泄漏
  • 循环引用问题Rc 形成环会 泄漏 (计数永远不为 0)
  • 常用 API:Rc::newRc::cloneRc::strong_countRc::weak_countRc::downgrade

示例:共享一段数据

use std::rc::Rc;
fn main() {
    // 1. 在堆上创建共享数据,初始计数为 1
    let config = Rc::new(String::from("核心配置程序"));
    
    println!("--- 初始状态 ---");
    println!("内容: {}", config);
    println!("引用计数: {}", Rc::strong_count(&config));

    // 2. 共享数据:创建两个新的指针指向同一块堆内存
    // Rc::clone 仅增加计数,不会拷贝字符串文本
    let user_a = Rc::clone(&config);
    let user_b = Rc::clone(&config);

    println!("\n--- 共享后 ---");
    println!("User A 看到的内容: {}", user_a);
    println!("User B 看到的内容: {}", user_b);
    println!("当前总引用计数: {}", Rc::strong_count(&config));

    // 3. 释放其中一个引用
    drop(user_a);
    println!("\n--- 释放 User A 后 ---");
    println!("剩余引用计数: {}", Rc::strong_count(&config));
}
  • 内存物理表现configuser_auser_b 是三个存储在栈上的指针,它们内部存储的内存地* 址完全相同,都指向堆上的同一个 String
  • 内容访问:由于实现了 Deref 特征,你可以像使用普通 String 一样直接打印 user_auser_b
  • 计数变化:每次调用 Rc::clone,计数器加 1;每次指针离开作用域(或被 drop),计数器减 1。

强引用 (Strong Reference) 与 弱引用 (Weak Reference)

这是管理 Rc 生命周期的两种手段:

  • 强引用 (Rc<T>)

    • 行为:使用 Rc::clone(&rc) 会增加强引用计数
    • 本质:它不是深拷贝(Deep Copy),只是在栈上多了一个指向堆内存的指针,并在堆上把计数器加 1。
    • 作用:只要强引用计数大于 0,堆上的数据就绝对不会被销毁。
  • 弱引用 (Weak<T>)

    • 行为:通过 Rc::downgrade(&rc) 创建。
    • 本质:它会增加弱引用计数,但不影响强引用计数。
    • 作用:它不拥有数据的所有权。即便还有 100 个 Weak 指针指向数据,只要强引用计数归零,数据依然会被清理。
    • 使用:因为数据可能已被销毁,使用前必须通过 weak_ptr.upgrade() 将其“升级”回 Option<Rc<T>> 进行检查。
use std::rc::Rc;
fn main() {
    // 1. 创建一个强引用 Rc
    let strong_ptr = Rc::new(String::from("Rust 智能指针"));
    
    // 2. 从强引用创建一个弱引用
    // downgrade 不会增加 strong_count
    let weak_ptr = Rc::downgrade(&strong_ptr);
    let weak_ptr2 = Rc::downgrade(&strong_ptr);

    println!("--- 初始状态 ---");
    println!("强引用计数: {}", Rc::strong_count(&strong_ptr)); // 1
    println!("弱引用计数: {}", Rc::weak_count(&strong_ptr));   // 2

    // 3. 尝试使用弱引用访问数据
    // 必须通过 upgrade() 升级为 Option<Rc<T>>
    match weak_ptr.upgrade() {
        Some(rc) => println!("弱引用升级成功,得到数据: {}", rc),
        None => println!("弱引用升级失败,数据已销毁"),
    }

    // 销毁一个弱引用
    drop(weak_ptr2);
    println!("销毁一个弱引用后强引用计数: {}", Rc::strong_count(&strong_ptr)); // 1
    println!("销毁一个弱引用后弱引用计数: {}", Rc::weak_count(&strong_ptr));   // 1

    println!("\n--- 销毁强引用后 ---");
    // 4. 手动销毁强引用(模拟离开作用域)
    drop(strong_ptr);

    // 5. 再次尝试使用弱引用访问数据
    // 此时强引用计数为 0,数据已被回收
    match weak_ptr.upgrade() {
        Some(rc) => println!("弱引用升级成功: {}", rc),
        None => println!("弱引用升级失败,数据已销毁"),
    }
}

Rc 环

循环引用问题:这是Rc 最危险的陷阱。

  • 成因:如果两个 Rc 指针互相指向对方(例如:节点 A 拥有指向节点 B 的 Rc,而节点 B 也拥有指向节点 A 的 Rc),就会形成一个环。
  • 后果:由于环的存在,这两个对象的强引用计数永远至少为 1。当外部作用域结束时,它们无法被清理,导致内存泄漏Memory Leak)。
  • 解决方案:将其中一条路径改为 Weak<T>。例如:父节点用 Rc 指向子节点(强引用),而子节点用 Weak 指向父节点(弱引用)。这样环就被打破了。

由于 Rc<T> 默认是只读的,为了在创建节点后能修改指针指向对方,我们需要配合使用 RefCell<T>演示环形引用。

use std::rc::Rc;
use std::cell::RefCell;
// 定义一个简单的 Node,它可以指向另一个 Node
#[derive(Debug)]
struct Node {
    next: RefCell<Option<Rc<Node>>>,
}
impl Drop for Node {
    fn drop(&mut self) {
        println!("Node 被销毁了!");
    }
}
fn main() {
    // 1. 创建两个节点 A 和 B
    let a = Rc::new(Node { next: RefCell::new(None) });
    let b = Rc::new(Node { next: RefCell::new(None) });

    println!("--- 建立环之前 ---");
    println!("A 的强引用计数: {}", Rc::strong_count(&a)); // 1
    println!("B 的强引用计数: {}", Rc::strong_count(&b)); // 1

    // 2. 建立环:A 指向 B,B 指向 A
    *a.next.borrow_mut() = Some(Rc::clone(&b));
    *b.next.borrow_mut() = Some(Rc::clone(&a));

    println!("--- 建立环之后 ---");
    println!("A 的强引用计数: {}", Rc::strong_count(&a)); // 2
    println!("B 的强引用计数: {}", Rc::strong_count(&b)); // 2

    // 3. 函数结束前,我们尝试让 a 和 b 离开作用域
    println!("--- main 函数即将结束 ---");
} 
// 正常情况下,这里应该打印两次 "Node 被销毁了!",但实际上什么都不会打印。

笔记:为什么会泄漏?

  • 计数器逻辑:当 main 函数结束时,变量 a 和 b 被丢弃。
    • a 被丢弃,其对应的堆内存计数从 2 降到 1。
    • b 被丢弃,其对应的堆内存计数从 2 降到 1。
  • 死循环:堆内存 A 还在等待堆内存 B 释放以便将其计数减为 0;而堆内存 B 也在等待堆内存 A 释放。
  • 结果:两个内存块的计数器永远卡在 1,drop 方法永远不会被触发,这块堆内存就泄漏了。

解决方案

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

// 一个“父-子”节点结构:
// - 子节点用 Rc 拥有(多个地方可以共享孩子)
// - 父指针用 Weak 指向父(避免形成 Rc 环)
#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,  // Weak<Node>:这是指向父节点的指针, Weak 不增加强引用计数
    children: RefCell<Vec<Rc<Node>>>,    // children 拥有子节点
}
impl Drop for Node {
    fn drop(&mut self) {
        println!("Node 被销毁了!");
    }
}
fn main() {
    let parent = Rc::new(Node {
        value: 1,
        parent: RefCell::new(Weak::new()),// 初始化时,父节点没有“父”,设为空弱引用
        children: RefCell::new(vec![]),// 初始化时,子节点列表为空向量
    });//将 Node 实例放入堆中,并返回一个智能指针,此时 parent 的强引用计数为 1,弱计数 = 0

    let child = Rc::new(Node {
        value: 2,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });//child 的强计数 = 1,弱计数 = 0

    // 建立父 -> 子 的强引用关系
    parent.children.borrow_mut().push(Rc::clone(&child));
    // 通过 borrow_mut() 获取 Vec 的可变借用,然后把 child 的一个克隆存进去。
    // 此时 child 的强引用计数变为 2(一个是变量 child,一个是 parent.children 里的克隆)

    // 建立子 -> 父 的弱引用关系(关键:避免环)
    *child.parent.borrow_mut() = Rc::downgrade(&parent);
    //通过 Rc::downgrade 将 parent 的强引用转为弱引用并存入 child.parent
    //parent 的强计数保持为 1。parent 的弱计数变为 1。

    println!("parent strong = {}, weak = {}",
        Rc::strong_count(&parent),
        Rc::weak_count(&parent)
    );
    println!("child strong = {}, weak = {}",
        Rc::strong_count(&child),
        Rc::weak_count(&child)
    );

    // Weak::upgrade:尝试把 Weak 变回 Rc(如果父节点已释放则返回 None)
    if let Some(p) = child.parent.borrow().upgrade() {
        println!("child's parent value = {}", p.value);
    } else {
        println!("parent already dropped");
    }
}

核心设计:为什么这样设计结构?

在 Rust 中,指针是表示内存地址的类型。为了实现父子双向链接,代码采用了“强弱结合”的策略:

  • children: RefCell<Vec<Rc<Node>>>(向下强引用):
    • 父节点需要“拥有”它的子节点,所以使用 Rc<Node>。只要父节点存在,子节点就不会被销毁。
  • parent: RefCell<Weak<Node>> (向上弱引用):
    • 这是破环关键。子节点只需要“知道”父节点是谁,但不应该“拥有”父节点。
    • 使用 Weak<Node> 不会增加父节点的强引用计数,因此不会阻止父节点被回收。
  • RefCell(内部可变性):
    • 因为节点创建时是孤立的,必须在创建后修改属性(建立连接),所以需要 RefCell 绕过编译期的不可变检查。
执行阶段Parent 计数 (Strong/Weak)Child 计数 (Strong/Weak)逻辑说明
创建后1 / 01 / 0变量 parent 和 child 各自拥有 1 个所有权
建立父->子连接1 / 02 / 0parent.children 存了一个 Rc::clone(&child),强计数加 1
建立子->父连接1 / 12 / 0child.parent 存了一个 downgrade(&parent),仅增加弱计数

常用 API 速查表

API功能描述
Rc::new(val)创建一个新的 Rc 实例,初始强计数为 1。
Rc::clone(&rc)增加强引用计数,返回新指针。
Rc::strong_count(&rc)查看当前的强引用数量。
Rc::weak_count(&rc)查看当前的弱引用数量。
Rc::downgrade(&rc)获取一个 Weak<T> 指针。
weak.upgrade()尝试将弱引用转回强引用,返回 Option<Rc<T>>

3. Arc<T>:多线程引用计数共享所有权

如果在多线程环境下使用 Rc<T>,编译器会直接报错,因为 Rc<T> 内部的引用计数是非原子的,无法在多个线程间安全地更新。为了解决这个问题,Rust 提供了 Arc<T>Atomic Reference Counted 原子引用计数),它允许在多个线程之间安全地共享同一个堆数据的所有权。和 Rc<T> 一样,Arc<T> 默认也是只读的。如果你需要多个线程同时修改数据,你还需要配合下一阶段我们要讲的锁机制(如 Mutex)。

在计算机中,普通的整数加减(如 count += 1)并不是一个不可分割的操作。如果两个线程同时尝试修改 Rc 的计数器,可能会导致计数错误,进而引发提前释放内存或内存泄漏。Arc<T> 使用了 原子操作来更新计数。这是一种硬件层面的特殊指令,能确保在多线程竞争时计数器依然准确。

你要会的点:

  • Arc = Atomic Rc(线程安全,计数操作是原子的)
  • 成本:比 Rc 更贵(原子操作),单线程场景优先 Rc以获得最佳性能
  • 常见组合:Arc<Mutex<T>>Arc<RwLock<T>>
  • 线程安全标志:
    • Rc<T> 未实现 SendSync 特征,因此不能跨线程传递
    • Arc<T> 实现了 SendSync(前提是内部的 T 也是线程安全的),可以在线程间自由穿梭

示例:多线程共享只读

use std::sync::Arc;
use std::thread;
fn main() {
    // 1. 创建一个 Arc 指针
    let data = Arc::new(String::from("多线程共享数据"));
    println!("Arc-data 初始计数: {}", Arc::strong_count(&data));
    let mut handles = vec![];
    for i in 0..5 {
        // 2. 克隆 Arc 指针:这只是增加原子计数,不拷贝字符串本身
        let data_clone = Arc::clone(&data);
        let handle = thread::spawn(move || {
            // 3. 在子线程中使用数据
            println!("线程 {} 看到的数据: {}", i, data_clone);
        });
        handles.push(handle);
    }
    // 等待所有线程结束
    for handle in handles {
        handle.join().unwrap();
    }
    println!("所有线程已完成,最后计数: {}", Arc::strong_count(&data));
}

4. 线程安全内部可变性:Mutex<T> / RwLock<T>

在多线程环境下,单靠 Arc<T> 只能解决“谁拥有数据”的问题,但由于 Arc<T> 提供的引用是不可变的,我们无法修改数据。为了在多线程中修改数据,我们需要使用“”。

你要会的点:

  • Mutex<T>:互斥锁,提供独占可变访问(锁守卫 MutexGuard
  • RwLock<T>:读写锁,多读单写
  • 死锁、锁粒度、持锁时间 (工程上很关键)
  • 常见组合:Arc<Mutex<T>>Arc<RwLock<T>>
  • 运行时成本:加锁/解锁、竞争、可能阻塞

示例:Mutex<T> (互斥锁)

MutexMutual Exclusion(互斥)的缩写。它保证在任何时刻,只有一个线程可以访问数据。

工作机制:

  1. 线程尝试通过 .lock() 获取锁。
  2. 如果锁已被占用,线程会阻塞(等待)。
  3. 获取成功后,返回一个 MutexGuard(智能指针)。
  4. 自动释放:由于 MutexGuard 实现了 Drop 特征,当它离开作用域时,锁会自动释放,无需手动解锁。
use std::sync::Mutex;
fn main() {
    let m = Mutex::new(5);
    {
        // lock() 返回一个 Result,因为如果另一个线程在持有锁时 panic,锁会变得“中毒”
        let mut num = m.lock().unwrap(); 
        *num = 6; // 通过解引用修改内部数据
    } // num 离开作用域,锁自动释放

    println!("m = {:?}", m);
}

示例:RwLock<T> (读写锁)

RwLock 代表 Read-Write Lock。它比 Mutex 更灵活,遵循“多读单写”规则:

  • 多读:允许多个线程同时持有只读锁 (.read())。
  • 单写:同一时间只允许一个线程持有写锁 (.write())。此时不允许任何读锁

适用场景:适用于“读多写少”的场景,性能通常优于 Mutex

Mutex<T> 的规则很简单:任何人要用,必须排队。 而 RwLock<T> 引入了“多读”逻辑,这虽然提高了并发效率,但也让逻辑变得复杂:你不能直接把一个读锁“升级”为写锁。你必须先释放读锁,然后再去竞争写锁

use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
fn main() {
    // 1. 使用 Arc 包装 RwLock,实现多线程共享所有权
    let lock = Arc::new(RwLock::new(5));
    let mut handles = vec![];
    // --- 模拟多个读取者 ---
    for i in 0..3 {
        let lock_clone = Arc::clone(&lock);
        let handle = thread::spawn(move || {
            // 获取读锁:允许多个线程同时进入此处
            let r = lock_clone.read().unwrap();
            println!("读者 {} 读取到的值: {}", i, *r);
            // 读锁在这里自动释放
        });
        handles.push(handle);
    }
    // --- 模拟一个写入者 ---
    {
        let lock_clone = Arc::clone(&lock);
        let handle = thread::spawn(move || {
            println!("写入者正在尝试获取写锁...");
            // 获取写锁:此时所有读锁和其他写锁都会被阻塞
            let mut w = lock_clone.write().unwrap();
            *w += 10;
            println!("写入者已将值修改为: {}", *w);
            // 写锁在这里自动释放
        });
        handles.push(handle);
    }
    // 等待所有线程结束
    for handle in handles {
        handle.join().unwrap();
    }

    println!("最终结果: {}", *lock.read().unwrap());
}

关键细节解析

  • .read().unwrap()

    • 返回一个 ReadGuard。只要这个 guard 存在,其他线程也可以调用 .read() 获取读锁。
    • 限制:一旦有人持有读锁,任何尝试调用 .write() 的线程都会进入睡眠等待状态。
  • .write().unwrap()

    • 返回一个 WriteGuard
    • 排他性:只有当没有任何人持有读锁且没有任何人持有写锁时,它才能成功获取。
  • 死锁风险

    • 如果在同一个线程中,你已经持有了读锁,又尝试去获取写锁,会导致死锁
    • 在写操作频繁的场景下,可能会出现“读者饥饿”。

总结对比:Mutex vs RwLock

特性Mutex<T>RwLock<T>
访问规则一次只能一个线程访问,多个读者 OR 一个写者
内部可变性是(通过锁获取可变引用)是(通过写锁获取可变引用)
线程安全
性能简单、开销固定读操作多时性能更好,管理成本略高

💡 笔记要点:为什么它们也叫“内部可变性”?

因为即使你只有一个指向 Mutex<T> 的不可变引用 &Mutex<T>,你依然可以通过调用 .lock() 方法来修改其内部包裹的数据。这种“外表不可变,内部可变”的特性正是它们被称为内部可变性的原因。

示例:多线程共享可变(Arc<Mutex<T>>

核心分工:

  • Arc(所有权管理者):解决“谁拥有数据”的问题。它让多个线程可以同时持有指向同一个堆内存的指针
  • Mutex(访问权限管理者):解决“谁能修改数据”的问题。它确保即使多个线程都有指针,同一时间也只有一个线程能真正碰到里面的数据
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
    let counter = Arc::new(Mutex::new(0));//最外层是 Arc,给这把锁加上引用计数功能,中间层是 Mutex,给数据加上一把锁
    let mut handles = vec![];
    for _ in 0..4 {
        let c = Arc::clone(&counter);//克隆的是指针地址和计数器,锁和数据始终只有一份
        handles.push(thread::spawn(move || {
            // lock() 返回一个 guard:guard 活着就持有锁
            let mut guard = c.lock().unwrap();
            *guard += 1;
            // 不需要手动写 unlock(), guard drop 时自动解锁(RAII)
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("counter = {}", *counter.lock().unwrap()); // 4
}

示例:多线程共享可变(读多写少Arc<RwLock<T>>

核心分工:

  • Arc:解决“谁拥有数据”的问题,确保堆内存地址在所有线程执行完毕前保持有效。
  • RwLock:解决“如何高效访问”的问题。它允许 100 个线程同时读取(并发),但只要有 1 个线程在写,其他人都必须等待(排他)。
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
fn main() {
    // 1. 初始化数据,Arc 负责跨线程共享,RwLock 负责并发权限
    let data = Arc::new(RwLock::new(0));
    let mut handles = vec![];
    let start_time = std::time::Instant::now();
    // 2. 开启 5 个读者线程
    for i in 0..5 {
        let d = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            // 获取读锁:注意!多个线程可以同时成功获取读锁
            let _guard = d.read().unwrap();
            println!("读者 {} 正在读取...", i);
            // 模拟一个耗时的读取操作(1秒)
            thread::sleep(Duration::from_secs(1)); 
            println!("读者 {} 读取完毕", i);
        }));
    }
    // 3. 开启 1 个写者线程
    let d_writer = Arc::clone(&data);
    handles.push(thread::spawn(move || {
        // 模拟等待一段时间再写,确保读者们已经先拿到了锁
        thread::sleep(Duration::from_millis(500));
        println!("写者:尝试获取写锁(会被阻塞,直到所有读者读完)...");
        // 获取写锁:必须等所有读锁释放
        let mut w = d_writer.write().unwrap();
        *w += 1;
        println!("写者:修改完毕!");
    }));
    for h in handles {
        h.join().unwrap();
    }
    println!("--- 总耗时: {:?} ---", start_time.elapsed());
}

代码逻辑深度拆解

  • 读锁并发 (.read().unwrap())
    • reader1 调用 read() 时,如果此时没有写线程,它会立即获得 ReadGuard
    • 优势:如果有 reader2、reader3 同时进来,只要当前没有线程持有“写锁”, 它们都可以获取读锁。这比 Mutex 快得多,因为 Mutex 会强制让读者也排队。
  • 写锁独占 (.write().unwrap()):
    • writer 调用 write() 时,写锁具有排他性, 它必须等待所有正在读取的线程释放读锁,且没有其他写线程在工作。反之,只要有任何读锁未释放,写锁获取也会被阻塞。

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

在掌握了 RcArc 之后,你可能已经发现它们默认是不可变共享的。为了在“不可变”的外壳下修改数据,我们需要内部可变性Interior Mutability)。

Rust 中,如果你使用普通引用,最怕的是:“我手里拿着指向数据的指针,结果别人把数据改了/删了,导致我手里的指针失效”。 但 Cell 完美避开了这个问题,因为它禁止你获取内部数据的指针:

  • 没有借用:你调用 c.get() 时,它直接给你一个全新的副本。你手里的副本和 Cell 里面的原始数据已经没关系了。
  • 随便覆盖:既然谁也拿不到指向 Cell 内部的指针,那么无论你调用多少次 set,都只是在修改那一块内存的值,不会破坏任何人的指针(因为根本没人持有指针)。

你要会的点:

  • Cell<T> 主要用于实现了 Copy 特征的类型(如 i32, bool, f64 等简单类型)
  • Cell 的操作逻辑不是“借用”,而是“值拷贝”。它不会给你内部数据的引用,而是让你把值取出来或存进去。
  • 与其它智能指针不同,Cell 不提供 .borrow().borrow_mut() 方法,只有两个核心动作:
    • .get():返回内部值的一个全新的副本Copy
    • .set(value):将新值拷贝进去,覆盖旧值
  • 应用场景:
    • 简单标志位:在结构体内部存储一些状态标记(如 is_valid: Cell<bool>),即使结构体是以不可变引用的形式传递,也能随时更新这些标记。
    • 性能敏感场景:如果你只需要操作简单的数字或布尔值,且不需要获取它们的引用,选 Cell 而不是 RefCell
use std::cell::Cell;
fn main() {
    // 即使变量 c 本身没有声明为 mut
    let c = Cell::new(10);
    // 我们可以通过不可变引用修改它的值
    let c_ref1 = &c;
    let c_ref2 = &c;
    c_ref1.set(20); 
    c_ref2.set(30);
    println!("当前值: {}", c.get()); // 输出 30
}

6. RefCell<T>:运行时借用检查(内部可变性)

如果说 Cell<T> 是通过“不给你指针”来保证安全,那么 RefCell<T>(Reference Cell) 就是通过雇佣一个运行时保安来让你安全地持有指针。

你要会的点:

  • 用于那些不方便拷贝(非 Copy 类型,比如 Vec 或自定义结构体),且你需要获取其引用的场景
  • 内部可变性(Interior Mutability) :即便外面是不可变绑定,也能在内部修改
  • 借用规则从编译时延后到运行时:
    • borrow() 获取一个不可变引用, 得到 Ref<T>
    • borrow_mut() 获取一个可变引用, 得到 RefMut<T>
    • 在程序运行时,RefCell 内部会维护一个计数器来记录当前的借用状态,运行时违反了规则则会 panic
  • Rc 组合:Rc<RefCell<T>> 是单线程常见共享可变方案

示例:共享 + 可变(单线程)

use std::cell::RefCell;

fn main() {
    // 数据包裹在 RefCell 中
    let data = RefCell::new(vec![1, 2, 3]);
    // 即使 data 是不可变的,我们也能获取可变借用
    {
        let mut mut_ref = data.borrow_mut();
        mut_ref.push(4);
    } // mut_ref 在这里离开作用域,借用标记被释放
    // 获取两个不可变借用
    let ref1 = data.borrow();
    let ref2 = data.borrow();
    println!("数据内容: {:?}", ref1);
}

为什么会 Panic?(违反规则的后果)

如果你尝试在同一个作用域内同时进行不可变和可变借用,RefCell 就会报错:

let data = RefCell::new(5);
let r1 = data.borrow();     // 运行时:不可变借用计数 +1
let r2 = data.borrow_mut(); // 运行时:发现已有不可变借用,直接 PANIC!

Cell<T>RefCell<T> 的深度对比

这是笔记中最重要的部分:

特性Cell<T>RefCell<T>
适用类型实现了 Copy 的简单类型任何类型(通常是较大的对象)
获取方式返回值的副本(.get()返回数据的引用(.borrow()
性能开销零开销(仅内存拷贝)有开销(运行时维护借用计数)
安全性编译时安全(无引用)运行时可能 Panic
线程安全不安全 (!Sync)不安全 (!Sync)

💡 为什么需要 RefCell

最常见的场景是:你实现了一个 Trait,而该 Trait 的方法签名要求使用不可变引用 &self,但你的具体实现却需要修改内部状态(比如缓存、日志记录等)

总结一句话

Cell 是“搬家”(拷贝值),RefCell 是“登记处”(在运行时盯着谁拿了钥匙,违规就报警)。

Rc<RefCell<T>>:单线程共享 + 可变

这是 Rust 单线程开发中最强大的组合拳。如果你理解了 Rc(解决所有权共享)和 RefCell(解决不可变外壳下的修改),那么把它们套在一起就得到了:一个可以被多个地方同时持有、且每个地方都能修改的“共享变量”。

Rust中,指针是表示内存地址的类型。为了实现“多处读写”这种在其他语言中很常见的行为,我们需要这种“套娃”结构。

核心逻辑

  • 外层 Rc<T>**:负责共享**。它允许多个变量持有指向同一块堆内存的指针,解决了“谁能拿到这块内存”的问题。
  • 内层 RefCell<T>**:负责修改**。它允许你在只有 Rc 提供的不可变引用的情况下,通过运行时检查来修改内部数据。

当你创建 let x = Rc::new(RefCell::new(5)) 时,内存中发生了以下情况:

  • 栈上:有一个 Rc 指针
  • 堆上:分配了一块空间,包含
    • Rc 的引用计数器
    • RefCell 的借用状态标志位
    • 实际的数据 T

代码示例:共享计数器

use std::rc::Rc;
use std::cell::RefCell;
fn main() {
    // 1. 创建共享的可变数据
    let shared_data = Rc::new(RefCell::new(vec![1, 2, 3]));
    // 2. 克隆 Rc 指针(增加强引用计数)
    let shared_data_clone = Rc::clone(&shared_data);
    // 3. 在一处修改
    {
        let mut mut_ref = shared_data.borrow_mut();
        mut_ref.push(4);
    } 
    // 4. 在另一处读取,发现数据已经变了
    println!("克隆端看到的数据: {:?}", shared_data_clone.borrow());
    // 输出: [1, 2, 3, 4]
}

深度对比:单线程 vs 多线程

这个组合有一个完美的“多线程对应版本”,请务必记在笔记中对比:

场景工具组合逻辑
单线程Rc<RefCell<T>>引用计数 + 运行时借用检查
多线程Arc<Mutex<T>>原子计数 + 互斥锁

风险警示:运行时崩溃

虽然这个组合很强,但它保留了 RefCell 的风险:

  • 如果你在同一个线程中,通过 shared_data.borrow() 拿到了一个引用还没放手,又尝试用 shared_data_clone.borrow_mut() 去修改,程序会直接 Panic
  • 它依然不是线程安全的,不能跨线程传递。

7. Cow<'a, T>:写时克隆(Copy-On-Write)

这是 Rust 中一个非常“聪明”且能显著提升性能的智能指针。Cow 的全称是 Copy-On-Write(写时克隆)。它的核心思想是:不到万不得已,绝不分配内存。将内存分配推迟到真正发生修改的那一刻。

核心定义

Cow 是一个枚举类型,包含两个变体:Borrowed(&'a T)Owned(T::Owned)。它允许你以一种统一的方式处理借用的数据和拥有的数据。

  • Borrowed(&'a T):持有数据的只读引用
  • Owned(T::Owned):持有数据的所有权(通常在堆上)

懒惰是美德

  • 初始状态: 当你创建一个 Cow 时,它通常从 Borrowed 开始。这不需要分配新内存,开销极小。
  • 读取数据:当你只需要读取时,它保持 Borrowed 状态,性能损耗为 O(1)
  • 修改数据:当你调用 .to_mut() 尝试修改数据时,Cow 会检查:如果已经是 Owned,直接返回引用。如果是 Borrowed,此时才会执行克隆(Clone),将数据变为 Owned,然后再让你修改。

你要会的点:

  • 表示“要么借用,要么拥有”
  • 读操作零拷贝,写操作才会 to_mut() 触发克隆
  • 常见场景:字符串处理、API 既接受 &str 又能返回 String 的优化
use std::borrow::Cow;
// 如果不需要修改,就借用输入(零拷贝);
// 如果需要修改,再转成拥有的 String。
fn normalize(s: &str) -> Cow<'_, str> {
    if s.contains(' ') {
        // 需要修改:分配新 String(Owned)
        Cow::Owned(s.replace(' ', "_"))
    } else {
        // 不需要修改:直接借用(Borrowed)
        Cow::Borrowed(s)
    }
}
fn main() {
    let a = normalize("hello");
    let b = normalize("hello world");

    println!("a = {}", a);
    println!("b = {}", b);
}

8. Pin<P>:禁止被移动(自引用/异步)

这是 Rust 中最“玄学”但也最底层、最重要的智能指针之一。如果说 Box 是为了放进堆里,那么 Pin 就是为了锁死内存地址

Define

A. 核心危机:自引用结构 (Self-referential Structs)

在 Rust 中,几乎所有类型都是可以移动的(Move)。这意味着如果你把一个变量传给另一个函数,它的内存地址可能会发生改变。但在某些特殊场景下,这种“移动”会导致程序崩溃。

想象一个结构体,它的一个字段是指向另一个字段的指针:

struct SelfRef {
    data: String,
    ptr: *const String, // 这个指针指向同一个结构体里的 data
}
  1. 初始状态ptr 记录了 data 在内存中的地址(比如 0x100)。
  2. 发生移动:如果你把这个结构体移动到堆上或者传给另一个线程,整个结构体的内存地址变了(变成了 0x200)。
  3. 结果data 的新地址是 0x200,但 ptr 仍然指着旧地址 0x100指针失效了! 这会导致未定义行为。

B. Pin<P> 的作用

Pin 的作用就是:给指针加一个约束,保证它指向的数据在内存中永远不会被移动。

  • 一旦数据被 Pin 住,你无法再通过 mem::swap 或移动所有权等方式改变它的物理位置。
  • 它主要配合 P(通常是 Box 或引用)使用,形成 Pin<Box<T>>Pin<&mut T>

C. 为什么异步(Async/Await)必须用它?

这是 Pin 最主要的应用场景。

  • 当你写 async 块时,编译器会将其转换为一个状态机(实现了 Future trait)。
  • 这个状态机内部经常包含自引用(例如:一个局部变量被引用后跨越了 .await 点)。
  • 因此,Futurepoll 方法签名必须是 fn poll(self: Pin<&mut Self>, ...)没有 Pin,异步代码就无法保证内存安全。

D. Unpin 特征:谁可以豁免?

并不是所有东西都需要被“锁死”。Rust 定义了一个标记特征 Unpin

  • Unpin 类型:绝大多数普通类型(如 i32, String, Box)。它们即便被移动也是安全的。对于这些类型,Pin<P> 没有任何实际限制,可以随意拿回可变引用。
  • !Unpin 类型:不能被移动的类型(如异步生成的 Future、自引用结构)。它们必须被 Pin 保护,否则编译器会阻止某些危险操作。

如何通俗理解 Pin

  • 普通指针:像一张写着地址的便签。你可以把便签传来传去,房子(数据)也可能搬家。
  • Pin 指针:像一颗钉子。它不仅告诉你地址,还将房子死死地钉在原地,不准搬家,直到房子被拆除(Drop)为止。

选型指南

  1. 如果你的数据不包含指向自身的指针,**不需要 Pin**
  2. 如果你在手写复杂的 Future 或者构建自引用底层库,**必须用 Pin**

这个示例只是让你看到 Pin<Box<T>>的写法;真正需要 Pin 的场景主要在 async/Future 或自引用结构里。

use std::pin::Pin;
fn main() {
    let x = Box::new(123);
    // Pin<Box<T>>:把堆上的 T “固定住”,承诺之后不会再移动它的内存地址
    let pinned: Pin<Box<i32>> = Box::pin(*x);
    // 访问内部值:as_ref 得到 Pin<&T>,get_ref 拿到 &T
    let r: &i32 = pinned.as_ref().get_ref();
    println!("pinned value = {}", r);
}

9. Box<dyn Trait> / Rc<dyn Trait>:trait object 与动态分发

这是 Rust 智能指针系列的最后一项核心应用。如果你需要处理不同类型但实现了相同特征的对象(例如一个数组里既有“圆形”又有“正方形”),你就必须用到 Box<dyn Trait>Rc<dyn Trait>

在 Rust 中,指针是表示内存地址的类型。由于不同类型的大小不同,我们无法直接在栈上存储“某个特征”,必须通过智能指针将其包装成 Trait Object

你要会的点:

  • 静态分发(泛型) vs 动态分发(trait object)
  • 对象安全(object safety)限制:哪些 trait 能变成 dyn Trait
  • fat pointer(数据指针 + vtable),大小/性能直觉

前置知识

A. 为什么必须用 BoxRc

特征(Trait)本身是 DST (Dynamically Sized Type),即“动态大小类型”。

  • 编译器在编译时不知道具体是哪个结构体实现了该特征,因此不知道它占用多少内存。
  • 解决方案:将对象放入堆中。Box<dyn Trait> 的大小是固定的(指针大小),无论堆上的对象实际有多大。

B. 物理结构:胖指针 (Fat Pointer)

当你使用 Box<dyn Trait> 时,这个指针在栈上占用 2 个单元(通常是 16 字节):

  1. 数据指针:指向堆内存中具体的对象数据。
  2. vtable 指针:指向一个“虚函数表”。表中记录了该特定类型实现该 Trait 的方法地址。

C. 静态分发 vs 动态分发

特性静态分发 (Generics <T: Trait>)动态分发 (dyn Trait)
原理编译时为每种类型生成一份代码(单态化)。运行时通过 vtable 查找函数地址。
性能极快。编译器可以进行内联优化。略慢。存在指针跳转开销,无法内联。
灵活性集合中只能存同一种类型。极高。集合中可以存多种不同类型。

D. 代码示例:不同类型的“绘图”

trait Draw {
    fn draw(&self);
}

struct Button { width: u32 }
impl Draw for Button {
    fn draw(&self) { println!("渲染按钮,宽度: {}", self.width); }
}

struct Image { url: String }
impl Draw for Image {
    fn draw(&self) { println!("渲染图片,源自: {}", self.url); }
}

fn main() {
    // 使用 Box<dyn Draw> 存储不同类型的对象
    let components: Vec<Box<dyn Draw>> = vec![
        Box::new(Button { width: 100 }),
        Box::new(Image { url: String::from("logo.png") }),
    ];

    for comp in components {
        comp.draw(); // 运行时动态分发:查表并调用对应方法
    }
}

什么时候用 Box<dyn Trait>

  1. 异质集合:当你需要一个 Vec 存储多种实现了相同接口的类型时。
  2. 解耦:当你不想在函数签名中暴露具体类型,只想表达“只要实现了这个特征就行”时。
  3. 减少编译时间:泛型会导致代码膨胀(单态化),而 dyn Trait 只有一份代码,可以缩短大型项目的编译时间。

10. 自定义智能指针(进阶)

你要会的点:

  • 如何实现自己的指针类型:
    • 实现 Deref/DerefMut,让它像引用一样用
    • 实现 Drop,在释放时执行资源回收
  • RAII 模式(文件句柄、网络连接、锁守卫等)
  • PhantomData(用于告诉编译器“我逻辑上拥有/借用某个 T”,影响 drop check/variance)——偏高级,但智能指针写多了会遇到

要自定义一个智能指针,本质上是创建一个结构体,并为它实现两个核心特征(Trait):DerefDrop。在 Rust 中,正是这两个特征让普通结构体拥有了“像指针一样工作”和“自动管理资源”的超能力。

1. 自定义智能指针的核心公式

  • Deref 特征:允许你通过解引用操作符(*)访问内部数据。
  • Drop 特征:定义当指针离开作用域时该执行什么清理逻辑(例如释放内存、关闭文件、打印日志)。

2. 实战演练:创建一个简单的 MyBox<T>

我们将模仿 Box<T> 的行为,创建一个能包裹数据的智能指针。

use std::ops::Deref;

//A. 定义结构体
struct MyBox<T>(T); // 元组结构体
impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

//B. 实现 `Deref` (让它能被 `*` 解引用)
//如果不实现 `Deref`,编译器就不知道执行 `*my_box` 时该返回什么。
impl<T> Deref for MyBox<T> {
    type Target = T; // 关联类型,指定解引用后得到的类型
    fn deref(&self) -> &Self::Target {
        &self.0 // 返回元组中的第一个元素引用
    }
}
//C. 实现 `Drop` (赋予它自动清理的能力)
impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        println!("MyBox 指针被销毁了,资源已释放!");
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);
    assert_eq!(5, x);
    assert_eq!(5, *y); // 这里触发了 y.deref()
    println!("y 的值是: {}", *y);
} // y 在这里离开作用域,触发 drop() 方法

3. 深度原理:Deref 强制转换 (Deref Coercion)

这是 Rust 智能指针极其好用的秘密武器。Deref 强制转换可以将一个实现了 Deref 的类型的引用转换为它内部类型的引用。

例子: 如果你有一个 MyBox<String>,Rust 可以自动将其转换为 &str

fn hello(name: &str) {
    println!("Hello, {}!", name);
}
fn main() {
    let m = MyBox::new(String::from("Rust"));
    // &m 是 &MyBox<String>
    // Rust 自动调用 deref 将其变为 &String
    // String 也实现了 Deref,再次调用 deref 变为 &str
    hello(&m); 
}

11. 选择指南

需求场景推荐指针备注
堆分配、大数据传递、递归类型Box<T>唯一所有权,开销最低。
单线程、多处共享只读数据Rc<T>引用计数。
多线程、多处共享只读数据Arc<T>原子引用计数,线程安全。
单线程、小对象内部可变 (Copy类型)Cell<T>get/set 值拷贝。
单线程、大对象内部可变 (非Copy)RefCell<T>运行时借用检查。
单线程、多所有权共享且可修改Rc<RefCell<T>>经典套娃组合。
多线程、共享且可修改Arc<Mutex<T>>并发黄金搭档。
避免循环引用、内存泄漏Weak<T>配合 Rc 或 Arc 使用。
性能优化、按需克隆Cow<T>写时克隆。
异步编程、锁死内存地址Pin<P>防止自引用结构移动。