第十章:智能指针——超越普通引用

第十章:智能指针——超越普通引用

本章导读:引用是 Rust 内存安全的基石,但有时我们需要更多的能力:共享所有权、延迟处理、自定义内存布局。智能指针就是这样的存在——它们看起来像引用,但拥有额外的超能力。本章将探索 Rust 生态中最重要的智能指针,理解它们如何扩展所有权模型。


🎯 10.1 什么是智能指针?

智能指针是表现类似指针的数据结构,但拥有额外的元数据和功能。

🔍 10.1.1 引用 vs 智能指针

特性引用 &T智能指针
所有权借用拥有
元数据生命周期可自定义
析构行为可自定义 Drop
开销可能有

📦 10.1.2 智能指针的模式

智能指针通常实现 DerefDrop trait:

use std::ops::Deref;

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

// Deref:让智能指针像引用一样使用
impl<T> Deref for MyBox<T> {
    type Target = T;
    
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

// Drop:自定义析构行为
impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        println!("MyBox 被释放了");
    }
}

📦 10.2 Box<T>:堆分配

Box 是最简单的智能指针,将数据存储在堆上。

🏠 10.2.1 基本用法

fn main() {
    // 在栈上存储指针,在堆上存储数据
    let b = Box::new(5);
    println!("b = {}", b);  // 自动解引用
    
    // 离开作用域时自动释放堆内存
}

🎯 10.2.2 使用场景

递归类型:解决类型大小未知的问题

// 编译失败:无限大小的类型
// enum List {
//     Cons(i32, List),
//     Nil,
// }

// 使用 Box:指针大小固定
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    
    fn print_list(list: &List) {
        match list {
            Cons(value, next) => {
                print!("{} ", value);
                print_list(next);
            }
            Nil => println!(),
        }
    }
    
    print_list(&list);  // 1 2 3
}

大型数据转移:避免栈拷贝

struct LargeArray([u8; 1000000]);

fn process(data: Box<LargeArray>) {
    // 只移动指针,不拷贝数据
}

fn main() {
    let data = Box::new(LargeArray([0u8; 1000000]));
    process(data);
}

Trait 对象:存储不同具体类型

trait Animal {
    fn speak(&self);
}

struct Dog;
impl Animal for Dog {
    fn speak(&self) { println!("汪汪!"); }
}

struct Cat;
impl Animal for Cat {
    fn speak(&self) { println!("喵喵!"); }
}

fn main() {
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog),
        Box::new(Cat),
    ];
    
    for animal in &animals {
        animal.speak();
    }
}

🔄 10.3 Rc<T>:共享所有权

Rc(Reference Counting)允许多个所有者共享同一数据。

📊 10.3.1 基本用法

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    
    // 克隆增加引用计数
    let a = Rc::clone(&data);
    let b = Rc::clone(&data);
    
    println!("引用计数: {}", Rc::strong_count(&data));  // 3
    
    // 所有引用都指向同一数据
    println!("a: {:?}", a);
    println!("b: {:?}", b);
    
    // 最后一个引用离开作用域时数据被释放
}

🌳 10.3.2 共享数据结构

use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: Vec<Rc<Node>>,
}

fn main() {
    let leaf = Rc::new(Node { value: 1, children: vec![] });
    
    let branch = Rc::new(Node {
        value: 2,
        children: vec![Rc::clone(&leaf)],  // leaf 被共享
    });
    
    // leaf 现在被两个地方引用
    println!("leaf 引用计数: {}", Rc::strong_count(&leaf));  // 2
    
    // 另一个分支也共享 leaf
    let another_branch = Node {
        value: 3,
        children: vec![Rc::clone(&leaf)],
    };
    
    println!("leaf 引用计数: {}", Rc::strong_count(&leaf));  // 3
}

⚠️ 10.3.3 注意事项

  • Rc 不是线程安全的,多线程环境使用 Arc
  • Rc 不可变,如需修改使用 RefCell
  • 小心循环引用,会导致内存泄漏

🧵 10.4 Arc<T>:线程安全的共享所有权

Arc(Atomic Reference Counting)是 Rc 的线程安全版本。

🔒 10.4.1 基本用法

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3, 4, 5]);
    
    let mut handles = vec![];
    
    for _ in 0..3 {
        let data_clone = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            println!("线程看到: {:?}", data_clone);
        }));
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

⚡ 10.4.2 性能考虑

Arc 使用原子操作,比 Rc 慢一些:

操作RcArc
Clone普通加法原子加法
Drop普通减法原子减法
访问直接直接

选择指南:单线程用 Rc,多线程用 Arc。在性能关键的场景,避免频繁克隆 Arc


🔓 10.5 RefCell<T>:内部可变性

RefCell 允许在不可变引用的情况下修改内部数据。

🎭 10.5.1 内部可变性模式

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(5);
    
    // data 是不可变的,但我们可以修改内部值
    *data.borrow_mut() += 1;
    
    println!("data = {}", data.borrow());
}

📖 10.5.2 借用规则:运行时检查

与引用的编译时检查不同,RefCell 在运行时检查借用规则:

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(5);
    
    // 正确:多个不可变借用
    {
        let r1 = data.borrow();
        let r2 = data.borrow();
        println!("{} {}", r1, r2);
    }
    
    // 正确:单个可变借用
    {
        let mut r = data.borrow_mut();
        *r += 1;
    }
    
    // 运行时 panic:同时存在可变和不可变借用
    // let r1 = data.borrow();
    // let mut r2 = data.borrow_mut();  // panic!
}

🎯 10.5.3 使用场景

use std::cell::RefCell;

struct Messenger {
    messages: RefCell<Vec<String>>,
}

impl Messenger {
    fn new() -> Self {
        Self {
            messages: RefCell::new(vec![]),
        }
    }
    
    // &self 是不可变引用,但可以修改内部数据
    fn send(&self, msg: &str) {
        self.messages.borrow_mut().push(msg.to_string());
    }
    
    fn get_messages(&self) -> std::cell::Ref<Vec<String>> {
        self.messages.borrow()
    }
}

fn main() {
    let messenger = Messenger::new();
    messenger.send("你好");
    messenger.send("世界");
    
    println!("消息: {:?}", messenger.get_messages());
}

🔄 10.6 组合使用:常见模式

🌳 10.6.1 Rc<RefCell<T>>:共享可变数据

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

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

impl Node {
    fn new(value: i32) -> Rc<Self> {
        Rc::new(Node {
            value,
            children: RefCell::new(vec![]),
        })
    }
    
    fn add_child(parent: &Rc<Self>, child: Rc<Self>) {
        parent.children.borrow_mut().push(child);
    }
}

fn main() {
    let root = Node::new(1);
    let child1 = Node::new(2);
    let child2 = Node::new(3);
    
    Node::add_child(&root, child1);
    Node::add_child(&root, child2);
    
    // 共享子节点
    let shared = Node::new(4);
    Node::add_child(&root.children.borrow()[0], Rc::clone(&shared));
    Node::add_child(&root.children.borrow()[1], Rc::clone(&shared));
    
    println!("根节点: {}", root.value);
    for child in root.children.borrow().iter() {
        println!("  子节点: {}", child.value);
        for grandchild in child.children.borrow().iter() {
            println!("    孙节点: {}", grandchild.value);
        }
    }
}

🧵 10.6.2 Arc<Mutex<T>>:线程安全的共享可变

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        }));
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("结果: {}", *counter.lock().unwrap());  // 10
}

比喻时刻Mutex 就像一个单人卫生间——同一时间只允许一个人进入(持有锁)。其他人必须等待当前的人离开(释放锁)后才能进入。


📚 10.7 其他智能指针

📝 10.7.1 Cow<T>:写时克隆

use std::borrow::Cow;

fn process(input: Cow<str>) -> Cow<str> {
    if input.contains("old") {
        // 需要修改时才分配新内存
        let mut owned = input.into_owned();
        owned = owned.replace("old", "new");
        Cow::Owned(owned)
    } else {
        // 不修改时直接返回引用
        input
    }
}

fn main() {
    let borrowed = Cow::Borrowed("hello old world");
    let result = process(borrowed);
    println!("{}", result);  // hello new world
    
    let no_change = Cow::Borrowed("hello world");
    let result2 = process(no_change);
    println!("{}", result2);  // hello world(未分配)
}

🎯 10.7.2 Cell<T>:简单内部可变性

use std::cell::Cell;

fn main() {
    let cell = Cell::new(5);
    
    // Cell 适用于 Copy 类型,开销更小
    cell.set(10);
    println!("值: {}", cell.get());
    
    // 原子替换
    let old = cell.replace(20);
    println!("旧值: {}, 新值: {}", old, cell.get());
}

📊 10.7.3 Weak<T>:避免循环引用

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

struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,   // 弱引用,不增加引用计数
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let root = Rc::new(Node {
        value: 1,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });
    
    let child = Rc::new(Node {
        value: 2,
        parent: RefCell::new(Rc::downgrade(&root)),  // 弱引用父节点
        children: RefCell::new(vec![]),
    });
    
    root.children.borrow_mut().push(Rc::clone(&child));
    
    // 通过弱引用访问父节点
    if let Some(parent) = child.parent.borrow().upgrade() {
        println!("父节点值: {}", parent.value);
    }
}

🧪 10.8 实战:构建带缓存的计算器

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

struct Calculator {
    cache: RefCell<HashMap<String, f64>>,
    operations: Vec<Box<dyn Fn(f64, f64) -> f64>>,
}

impl Calculator {
    fn new() -> Self {
        Self {
            cache: RefCell::new(HashMap::new()),
            operations: vec![
                Box::new(|a, b| a + b),
                Box::new(|a, b| a - b),
                Box::new(|a, b| a * b),
                Box::new(|a, b| if b != 0.0 { a / b } else { f64::NAN }),
            ],
        }
    }
    
    fn compute(&self, a: f64, op_idx: usize, b: f64) -> f64 {
        let key = format!("{}_{}_{}", a, op_idx, b);
        
        // 检查缓存
        if let Some(&result) = self.cache.borrow().get(&key) {
            println!("从缓存获取: {} = {}", key, result);
            return result;
        }
        
        // 计算
        let result = if let Some(op) = self.operations.get(op_idx) {
            op(a, b)
        } else {
            f64::NAN
        };
        
        // 存入缓存
        self.cache.borrow_mut().insert(key.clone(), result);
        println!("计算并缓存: {} = {}", key, result);
        
        result
    }
    
    fn clear_cache(&self) {
        self.cache.borrow_mut().clear();
        println!("缓存已清空");
    }
    
    fn cache_size(&self) -> usize {
        self.cache.borrow().len()
    }
}

fn main() {
    let calc = Rc::new(Calculator::new());
    
    println!("=== 第一次计算 ===");
    println!("2 + 3 = {}", calc.compute(2.0, 0, 3.0));
    println!("5 - 1 = {}", calc.compute(5.0, 1, 1.0));
    
    println!("\n=== 重复计算(命中缓存)===");
    println!("2 + 3 = {}", calc.compute(2.0, 0, 3.0));
    
    println!("\n=== 当前缓存大小: {} ===", calc.cache_size());
    
    // 共享计算器
    let calc2 = Rc::clone(&calc);
    println!("\n=== 在另一个引用上计算 ===");
    println!("4 * 5 = {}", calc2.compute(4.0, 2, 5.0));
    println!("缓存大小: {}", calc.cache_size());
    
    // 清空缓存
    calc2.clear_cache();
    println!("清空后缓存大小: {}", calc.cache_size());
}

📝 本章小结

本章我们学习了 Rust 的智能指针:

  • <code>Box&lt;T&gt;</code> 堆分配,用于递归类型和 Trait 对象
  • <code>Rc&lt;T&gt;</code> 引用计数共享所有权(单线程)
  • <code>Arc&lt;T&gt;</code> 原子引用计数共享所有权(多线程)
  • <code>RefCell&lt;T&gt;</code> 内部可变性,运行时借用检查
  • <code>Mutex&lt;T&gt;</code> 互斥锁,线程安全的内部可变性
  • <code>Weak&lt;T&gt;</code> 弱引用,避免循环引用

关键要点:

  • 智能指针拥有数据,而不仅仅是借用
  • Deref trait 让智能指针像引用一样使用
  • Drop trait 自定义析构行为
  • 组合使用满足复杂场景需求
  • 注意循环引用导致的内存泄漏

在下一章,我们将学习并发编程——Rust 的"无畏并发"哲学。


动手实验

  1. 使用 Rc<RefCell<T>> 实现一个双向链表节点结构。
  2. 使用 Arc<Mutex<T>> 实现一个多线程安全的计数器,让 5 个线程各增加 1000 次。
  3. 解释为什么 RefCell 的借用检查在运行时而非编译时,这有什么优缺点?
← 返回目录