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());
}
代码解释:
- Rc 负责让 shared_data, owner_1, owner_2 三个变量都能安全地“指向”同一个 RefCell,而不会导致所有权问题。
- RefCell 负责让任何一个所有者在需要时,都能获得对内部 Vec
的可变访问权限(通过 .borrow_mut())。 - owner_1 的修改对 owner_2 是可见的,因为它们操作的是堆上完全相同的那个 Vec。
4. Arc<Mutex> - 线程安全的共享与可变
这是多线程版本的 Rc<RefCell
场景: 启动多个线程,让它们同时对一个计数器进行增加操作。如果没有同步机制,最终结果将是错误的。
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)极大地保证了并发安全,避免了数据竞争。