1. Rc - 共享所有权

这个例子将创建一个链表,然后创建另一个分支链表,它们将共享第一个链表的后半部分。这可以高效地使用内存,因为我们没有复制数据,只是共享了它。

场景: 你有两个列表 a 和 b。列表 b 的开头是 3,然后它想接上列表 a 的全部内容。

use std::rc::Rc;

// 定义一个简单的链表结构
// Cons(值, 指向下一个节点的 Rc 指针)
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    // 创建一个列表 a: 5 -> 10 -> Nil
    // Rc::new 创建了一个新的 Rc 智能指针
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("创建 a 后的 a 的引用计数 = {}", Rc::strong_count(&a)); // 输出 1

    // 创建列表 b,它以 3 开头,然后链接到 a
    // Rc::clone 并不会深度拷贝 a 的数据,它只是创建另一个指向相同内存的指针
    // 并将引用计数加 1
    let b = Cons(3, Rc::clone(&a));
    println!("创建 b 后的 a 的引用计数 = {}", Rc::strong_count(&a)); // 输出 2

    {
        // 创建列表 c,它以 4 开头,也链接到 a
        let c = Cons(4, Rc::clone(&a));
        println!("创建 c 后的 a 的引用计数 = {}", Rc::strong_count(&a)); // 输出 3
    } // c 在这里被销毁,它对 a 的引用也消失了,所以引用计数减 1

    println!("c 离开作用域后的 a 的引用计数 = {}", Rc::strong_count(&a)); // 输出 2
}

代码解释:

  • Rc 让多个变量可以“共享”一份数据的所有权。
  • Rc::clone(&a) 只是增加了引用计数,而不是复制整个列表。这非常高效。
  • 当一个 Rc 指针离开作用域时(比如 c),它的引用计数会自动减一。
  • 只有当引用计数变为 0 时,堆上的数据(我们的 Cons 节点)才会被真正地清理掉。
  • 这个例子只能在单线程中工作。

2. RefCell - 内部可变性

这个例子将创建一个 Mock(模拟)对象用于测试。这个 Mock 对象对外表现为不可变的(它的方法接收 &self),但它内部需要记录被调用的情况,这就需要内部可变性。

场景: 我们有一个发信服务 Messenger,我们想测试一个功能:当某个值超过限额的 90% 时,Messenger 是否正确地发送了警告消息。

use std::cell::RefCell;

// 这是我们要测试的外部服务接口
pub trait Messenger {
    // 注意这里是 &self,表示不可变借用
    fn send(&self, msg: &str);
}

// 这是我们的 Mock 对象,用于测试
struct MockMessenger {
    // 我们用 RefCell 包裹一个 Vec,来记录所有被“发送”的消息
    // 即使在 &self 方法中,我们也能修改这个 Vec
    sent_messages: RefCell<Vec<String>>,
}

impl MockMessenger {
    fn new() -> MockMessenger {
        MockMessenger {
            sent_messages: RefCell::new(vec![]),
        }
    }
}

impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
        // .borrow_mut() 在运行时获取一个可变引用
        // 如果此时已经有其他可变或不可变引用,程序会 panic
        self.sent_messages.borrow_mut().push(String::from(message));
    }
}

// --- 测试代码 ---
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_sends_an_over_90_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        // limit_tracker 需要一个实现了 Messenger 的东西

        // 假设这里有一些使用 messenger 的逻辑...
        // 我们直接调用 send 来模拟
        let warning_msg = "Warning: You've used up over 90% of your quota!";
        mock_messenger.send(warning_msg);

        // .borrow() 获取一个不可变引用来检查结果
        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
        assert_eq!(mock_messenger.sent_messages.borrow()[0], warning_msg);
    }
}

fn main() {
    // 这个 main 函数只是为了让代码可以运行,核心逻辑在测试中
    println!("这是一个 RefCell 的例子,请运行 `cargo test` 查看结果");
}

代码解释:

  • MockMessenger::send 方法签名是 &self,通常情况下我们无法修改 self.sent_messages。
  • 但因为 sent_messages 被 RefCell 包裹,我们可以调用 .borrow_mut() 来“绕过”编译器的检查,在运行时获得一个可变引用。
  • 这使得我们可以在测试中追踪 send 方法是否被正确调用,以及传入了什么参数。
  • 这是 RefCell 的一个核心用途:在无法(或不方便)使用 &mut self 的地方实现内部状态的修改。

3. Rc<RefCell> - 共享且可变的数据

这是单线程中最强大的组合。Rc 允许多个所有者,RefCell 允许内部可变性。结合起来,就是多个所有者可以修改同一个数据。

场景: 想象一个应用中,有多处代码需要访问和修改一个共享的配置或状态。

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

fn main() {
    // 创建一个被 Rc 和 RefCell 共同包裹的数据
    // 初始值是一个包含 5 的 Vec
    let shared_data: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(vec![5]));

    println!("初始引用计数: {}", Rc::strong_count(&shared_data)); // 1

    // 模拟第一个所有者
    let owner_1 = Rc::clone(&shared_data);
    println!("owner_1 克隆后引用计数: {}", Rc::strong_count(&shared_data)); // 2

    // 模拟第二个所有者
    let owner_2 = Rc::clone(&shared_data);
    println!("owner_2 克隆后引用计数: {}", Rc::strong_count(&shared_data)); // 3

    // 第一个所有者通过 borrow_mut 修改数据
    owner_1.borrow_mut().push(10);
    println!("owner_1 修改后, 数据为: {:?}", owner_1.borrow());

    // 第二个所有者也能看到这个修改,并继续修改
    owner_2.borrow_mut().push(20);

    // 打印最终数据,可以看到所有修改都生效了
    println!("最终共享数据: {:?}", shared_data.borrow());
    // 或者通过任何一个 owner 访问
    println!("通过 owner_2 访问最终数据: {:?}", owner_2.borrow());
}

代码解释:

  1. Rc 负责让 shared_data, owner_1, owner_2 三个变量都能安全地“指向”同一个 RefCell,而不会导致所有权问题。
  2. RefCell 负责让任何一个所有者在需要时,都能获得对内部 Vec 的可变访问权限(通过 .borrow_mut())。
  3. owner_1 的修改对 owner_2 是可见的,因为它们操作的是堆上完全相同的那个 Vec。

4. Arc<Mutex> - 线程安全的共享与可变

这是多线程版本的 Rc<RefCell>,也是 Rust 并发编程中最核心的模式。

场景: 启动多个线程,让它们同时对一个计数器进行增加操作。如果没有同步机制,最终结果将是错误的。

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

fn main() {
    // Arc: Atomic Reference Counted,线程安全的引用计数
    // Mutex: Mutual Exclusion,互斥锁,保证一次只有一个线程能访问数据
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    // 启动 10 个线程
    for _ in 0..10 {
        // 为每个线程克隆 Arc 指针
        // 这和 Rc::clone 类似,但它是原子操作,是线程安全的
        let counter_clone = Arc::clone(&counter);

        let handle = thread::spawn(move || {
            // 获取互斥锁。
            // .lock() 会阻塞当前线程,直到它能获得锁为止。
            // .unwrap() 在这里用于处理一种特殊情况:如果持有锁的线程 panic 了,
            // 再次获取锁会失败,此时程序会 panic。
            let mut num = counter_clone.lock().unwrap();

            // 获得锁之后,我们得到一个 MutexGuard (num)
            // 我们可以像普通的可变引用一样操作它
            *num += 1;

            // 当 num (MutexGuard) 在这个闭包的末尾离开作用域时,
            // 锁会自动被释放,其他等待的线程就有机会获得锁了。
        });
        handles.push(handle);
    }

    // 等待所有线程执行完毕
    for handle in handles {
        handle.join().unwrap();
    }

    // 所有线程都执行完毕后,打印最终结果
    // 我们在主线程也需要获取锁来读取最终的值
    println!("Result: {}", *counter.lock().unwrap()); // 应该输出 10
}

代码解释:

  • Arc 使得多个线程可以共同“拥有”指向同一个 Mutex 的指针。
  • Mutex 像一个“房间”,数据在房间里。任何线程想操作数据,必须先拿到房间唯一的钥匙(调用 .lock())。
  • 一个线程拿到钥匙后,其他线程就必须在门外排队等待。
  • 线程用完后离开作用域,会自动把钥匙还回来(MutexGuard 被销毁),这样排队的下一个线程才能进去。
  • 这个“加锁-操作-自动解锁”的模式(RAII)极大地保证了并发安全,避免了数据竞争。