练习与自测
本章练习共 11 题,答案折叠在每题下方。建议先自己写、编译通过后再展开答案;难度标记:★☆☆ 基础 / ★★☆ 综合 / ★★★ 挑战。
练习 1:实现智能指针 MyBox
难度:★★☆
要求:实现泛型智能指针 MyBox<T>,要求:
MyBox::new(x)构造;- 实现
Deref,使其能用于需要&T的地方(用fn str_len(s: &str) -> usize验证解引用强制转换); - 实现
DerefMut,使*boxed = new_value能编译; - 实现
Drop,析构时打印释放 <值>,并解释为什么Drop里的字段仍然会被自动释放。
提示:Deref 返回 &self.0,DerefMut 返回 &mut self.0。
参考答案(先自己写再看)
参考答案(先自己写再看)
rust
use std::fmt;
use std::ops::{Deref, DerefMut};
struct MyBox<T: fmt::Display>(T);
impl<T: fmt::Display> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T: fmt::Display> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T: fmt::Display> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<T: fmt::Display> Drop for MyBox<T> {
fn drop(&mut self) {
println!("释放 {}", self.0);
}
}
fn str_len(s: &str) -> usize {
s.len()
}
fn main() {
let mb = MyBox::new(String::from("hello"));
println!("长度 = {}", str_len(&mb)); // &MyBox<String> -> &String -> &str
println!("大写 = {}", mb.to_uppercase()); // 自动解引用调用 String 方法
let mut num = MyBox::new(7);
*num = 42; // DerefMut
println!("num = {}", *num);
drop(mb);
println!("mb 已提前释放");
}输出:
长度 = 5
大写 = HELLO
num = 42
释放 hello
mb 已提前释放
释放 42要点解析:
Deref::deref返回&self.0,所有权仍在MyBox里,所以MyBox依然是唯一所有者。- 解引用强制转换发生在「目标类型已知」的位置:
str_len(&mb)里编译器知道要&str, 于是连续插入两次deref(&MyBox<String>→&String→&str)。 Drop::drop只负责额外清理;字段self.0仍然由编译器在drop之后自动释放。 如果T自己也实现了Drop,你会看到「先打印本体的『释放 …』,再打印字段的析构」。*num = 42需要DerefMut;只实现Deref会报E0596。
练习 2:Rc<RefCell<T>> 共享可变数据
难度:★★☆
要求:用 Rc<RefCell<T>> 实现一个可共享可变的待办列表:
TodoList内部的Vec<Rc<RefCell<Todo>>>;Todo { title: String, done: bool };- 提供
add(&self, title: &str) -> Rc<RefCell<Todo>>(注意&self)、complete(&self, item: &Rc<RefCell<Todo>>)、list(&self); - 在
main里对同一个Todo从两个「视图」(两个Vec)访问并修改,证明共享可见。
提示:内部用 RefCell<Vec<..>> 才能在 &self 下 push。
参考答案(先自己写再看)
参考答案(先自己写再看)
rust
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Todo {
title: String,
done: bool,
}
#[derive(Default)]
struct TodoList {
items: RefCell<Vec<Rc<RefCell<Todo>>>>, // RefCell 才能在 &self 下 push
}
impl TodoList {
fn new() -> TodoList {
TodoList::default()
}
// 注意是 &self:内部可变性让我们不需要 &mut self
fn add(&self, title: &str) -> Rc<RefCell<Todo>> {
let item = Rc::new(RefCell::new(Todo {
title: title.to_string(),
done: false,
}));
self.items.borrow_mut().push(Rc::clone(&item));
item
}
fn complete(&self, item: &Rc<RefCell<Todo>>) {
item.borrow_mut().done = true;
}
fn list(&self, label: &str) {
println!("--- {label} ---");
for it in self.items.borrow().iter() {
let t = it.borrow();
println!("[{}] {}", if t.done { "x" } else { " " }, t.title);
}
}
}
fn main() {
let list = TodoList::new();
let write_docs = list.add("写文档");
list.add("跑测试");
// 第二个「视图」:另一个容器持有同一批 Todo
let urgent: Vec<Rc<RefCell<Todo>>> = vec![Rc::clone(&write_docs)];
list.complete(&urgent[0]); // 从第二个视图标记完成
list.list("待办列表");
urgent[0].borrow_mut().title.push_str("(已复核)");
list.list("从另一视图改名后");
println!("write_docs 的强引用数 = {}", Rc::strong_count(&write_docs));
}输出:
--- 待办列表 ---
[x] 写文档
[ ] 跑测试
--- 从另一视图改名后 ---
[x] 写文档(已复核)
[ ] 跑测试
write_docs 的强引用数 = 3要点解析:
add用&self就能改items,靠的是RefCell把借用检查推到运行期。Rc::clone只加计数,urgent和items指向同一个Todo;所以从urgent[0]改,list.list()立刻能看到(别名可见,和 Java 引用一样)。- 强引用数是 3:
items里一份、urgent里一份、局部变量write_docs一份。 - 常见坑:写成
for it in self.items.borrow().iter() { self.items.borrow_mut()... }会在运行期 panic——遍历时的Ref还活着。需要修改就先收集再改(见练习 7)。
练习 3:用 Weak 避免引用循环
难度:★★★
要求:用 Rc + Weak 实现一个双向链表(每个节点有 prev / next),要求:
prev用Weak,next用Option<Rc<..>>;- 提供
push_back、prev_of、next_of; - 在
main里打印strong_count/weak_count,并在drop掉尾部节点后证明 无循环泄漏(给节点加Drop打印,或用计数说明)。
提示:prev 存 Weak 时 downgrade 不会增加强引用;upgrade() 返回 Option。
参考答案(先自己写再看)
参考答案(先自己写再看)
rust
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
prev: RefCell<Weak<Node>>, // 反向指针用 Weak,避免成环
next: RefCell<Option<Rc<Node>>>, // 正向指针用 Rc,表示拥有
}
impl Node {
fn new(value: i32) -> Rc<Node> {
Rc::new(Node {
value,
prev: RefCell::new(Weak::new()),
next: RefCell::new(None),
})
}
fn push_back(tail: &Rc<Node>, value: i32) -> Rc<Node> {
let node = Node::new(value);
*node.prev.borrow_mut() = Rc::downgrade(tail);
*tail.next.borrow_mut() = Some(Rc::clone(&node));
node
}
fn prev_of(node: &Rc<Node>) -> Option<Rc<Node>> {
node.prev.borrow().upgrade()
}
fn next_of(node: &Rc<Node>) -> Option<Rc<Node>> {
node.next.borrow().as_ref().map(Rc::clone)
}
}
impl Drop for Node {
fn drop(&mut self) {
println!("drop Node({})", self.value);
}
}
fn main() {
let head = Node::new(1);
let mid = Node::push_back(&head, 2);
let tail = Node::push_back(&mid, 3);
println!("--- 计数 ---");
for (name, n) in [("head", &head), ("mid", &mid), ("tail", &tail)] {
println!(
"{name}: value={}, strong={}, weak={}",
n.value,
Rc::strong_count(n),
Rc::weak_count(n)
);
}
println!("--- 导航 ---");
println!("mid.prev = {:?}", Node::prev_of(&mid).map(|n| n.value));
println!("mid.next = {:?}", Node::next_of(&mid).map(|n| n.value));
println!("--- 释放尾部,验证不会级联泄漏 ---");
let weak_tail = Rc::downgrade(&tail);
drop(tail);
println!("tail 已 drop,weak_tail.upgrade() = {:?}", weak_tail.upgrade().is_some());
println!("--- main 结束,head/mid 逆序释放 ---");
}输出:
--- 计数 ---
head: value=1, strong=1, weak=1
mid: value=2, strong=2, weak=1
tail: value=3, strong=2, weak=0
--- 导航 ---
mid.prev = Some(1)
mid.next = Some(3)
--- 释放尾部,验证不会级联泄漏 ---
drop Node(3)
tail 已 drop,weak_tail.upgrade() = false
--- main 结束,head/mid 逆序释放 ---
drop Node(2)
drop Node(1)要点解析:
mid的strong=2:局部变量mid一份 +head.next里一份。tail的strong=2:局部变量 +mid.next。head只有 1 份(局部变量)。head的weak=1来自mid.prev;mid的weak=1来自tail.prev;tail的weak=0因为没人指向它。drop(tail)后Node(3)立即析构(强引用归零),此时weak_tail还在, 所以控制块保留,upgrade()安全地返回None——这正是Weak存在的意义。- 关键:
prev必须是Weak。如果mid.prev写成Rc<Node>,head ↔ mid立刻成环, 上面所有drop Node(...)一行都不会打印。
练习 4:闭包与环境:带缓存的装饰器
难度:★★☆
要求:实现一个接收 impl Fn(u64) -> u64 的带缓存的装饰器 cached(f), 返回一个 impl Fn(u64) -> u64,内部用 RefCell<HashMap<u64, u64>> 记忆化。 用 main 证明「同一个输入只计算一次」(在 f 里打印一行)。
提示:cached 必须 move 捕获 cache;返回的闭包只用 &self 访问 cache,所以是 Fn。
参考答案(先自己写再看)
参考答案(先自己写再看)
rust
use std::cell::RefCell;
use std::collections::HashMap;
fn cached<F>(f: F) -> impl Fn(u64) -> u64
where
F: Fn(u64) -> u64,
{
let cache: RefCell<HashMap<u64, u64>> = RefCell::new(HashMap::new()); // move 进闭包
move |n| {
// 先查缓存:这里只需要 &self,所以整个闭包是 Fn 而不是 FnMut
if let Some(hit) = cache.borrow().get(&n) {
return *hit;
}
let value = f(n);
cache.borrow_mut().insert(n, value);
value
}
}
fn slow_double(n: u64) -> u64 {
println!(" 真的计算了一次 double({n})");
n * 2
}
fn main() {
let memo = cached(slow_double);
println!("第一次: {}", memo(21));
println!("第二次: {}", memo(21)); // 命中缓存,不会再打印「真的计算」
println!("第三次: {}", memo(30));
// memo 是 Fn,所以可以同时被多处只读调用(甚至并发不行——RefCell 不是 Sync)
let also: &dyn Fn(u64) -> u64 = &memo;
println!("通过引用调用: {}", also(21));
}输出:
真的计算了一次 double(21)
第一次: 42
第二次: 42
真的计算了一次 double(30)
第三次: 60
通过引用调用: 42要点解析:
cache必须被move进闭包,否则函数返回后cache就被析构了(E0373)。- 输出的关键在「第二次调用
memo(21)没有再打印『真的计算了一次』」——缓存命中了; 换成新参数30时才会重新计算。 - 返回类型写
impl Fn而不是impl FnMut:因为闭包内部只用&self的方式访问cache(RefCell把可变性藏在了共享引用后面),编译器因此把它判定为Fn。 这比FnMut更宽松,调用方不需要mut绑定。 - 常见坑:写
cache.borrow_mut().entry(n).or_insert_with(...)时,闭包里又要f(n)又要改cache,容易触发BorrowMutError。分成「查—算—写」三步最稳。
练习 5:观察者模式与内部可变性
难度:★★☆
要求:写一个 Subject / 观察者结构:Subject 持有 listeners: RefCell<Vec<Rc<dyn Fn(&str)>>>,提供 subscribe(&self, f) 与 set(&self, v: &str); 两个订阅者分别记录到 Rc<RefCell<Vec<String>>> 与累加字符数。 说明为什么这里用 RefCell 而不是 Cell。
提示:Rc<dyn Fn(&str)> 需要类型标注,否则闭包类型无法统一。
参考答案(先自己写再看)
参考答案(先自己写再看)
rust
use std::cell::RefCell;
use std::rc::Rc;
type Listener = Rc<dyn Fn(&str)>;
#[derive(Default)]
struct Subject {
value: RefCell<String>,
listeners: RefCell<Vec<Listener>>,
}
impl Subject {
fn subscribe(&self, f: Listener) {
self.listeners.borrow_mut().push(f);
}
fn set(&self, v: &str) {
*self.value.borrow_mut() = v.to_string();
// 先 clone 出监听器列表,避免在回调里 subscribe 造成 RefCell 二次借用冲突
let snapshot: Vec<Listener> = self.listeners.borrow().clone();
for l in &snapshot {
l(v);
}
}
}
fn main() {
let subject = Subject::default();
let history: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
let sink = Rc::clone(&history);
subject.subscribe(Rc::new(move |v: &str| {
sink.borrow_mut().push(v.to_string());
}));
let total_chars = Rc::new(RefCell::new(0usize));
let acc = Rc::clone(&total_chars);
subject.subscribe(Rc::new(move |v: &str| {
*acc.borrow_mut() += v.len();
}));
subject.set("alpha");
subject.set("beta");
println!("history = {:?}", history.borrow());
println!("total_chars = {}", total_chars.borrow());
}输出:
history = ["alpha", "beta"]
total_chars = 9要点解析:
- 为什么不是
Cell:Cell::get()要求T: Copy,Vec<Listener>显然不是Copy; 而且我们要在遍历时拿到元素的引用(&dyn Fn(&str))来调用,Cell根本不允许 把内部值的引用借出去(只能整体take/replace)。RefCell提供borrow()返回Ref<Vec<..>>,可以安全地遍历。 total_chars = 9来自"alpha"(5 个字符)+"beta"(4 个字符)。set里先clone出snapshot是因为:如果在遍历listeners的Ref期间, 某个回调又调用subscribe(要borrow_mut),就会运行期 panic。这个「先快照再回调」 是观察者/事件系统里最常见的防 panic 写法。Rc<dyn Fn(&str)>是必需的:两个闭包是不同类型,只有 trait 对象能放进同一个Vec。
练习 6:Cell 与 RefCell 的选型判断
难度:★☆☆
要求(选型题):下面四个字段分别该用 Cell 还是 RefCell?说明理由。
request_count: ???<u64>(fn serve(&self)里自增)log_buffer: ???<Vec<String>>(fn log(&self, msg: &str)里 push)is_dirty: ???<bool>(fn render(&self)里读、fn mark_dirty(&self)里置 true)cache: ???<HashMap<String, Vec<u8>>>(fn get_or_load(&self, k: &str) -> Vec<u8>)
提示:问自己两个问题——「能不能整体复制?」和「需不需要拿到内部 &mut?」。
参考答案(先自己写再看)
参考答案(先自己写再看)
| 字段 | 选择 | 理由 |
|---|---|---|
request_count: u64 | Cell<u64> | u64: Copy,get/set 各一条指令,零开销 |
log_buffer: Vec<String> | RefCell<Vec<String>> | 需要在 &self 下 push(要 &mut Vec);Vec 不是 Copy,Cell 的 get 不可用 |
is_dirty: bool | Cell<bool> | bool: Copy,语义就是「整体读/写」,没有借用内部的需求 |
cache: HashMap<String, Vec<u8>> | RefCell<HashMap<..>> | 需要 get/entry 拿内部 &mut(或至少拿 & 长期存活),HashMap 也不 Copy |
两个判断问题:
- 能不能整体复制?
T: Copy才有资格考虑Cell(Cell::get的要求)。 - 需不需要内部值的引用? 只要需要
&T或&mut T(遍历、push、entry、 传给别的函数),就必须RefCell;Cell的 API 刻意不提供借出内部引用的能力。
补充两点:
- 如果
T是Vec<u8>这种「需要整体搬走」的,Cell<T>+take/replace也能用 (要求T: Default或提供替代值),但代码更笨重,只有在性能敏感的极热路径上才值得。 - 这两个类型都是
!Sync。如果字段要在多线程里访问,换成Mutex<T>/ 原子类型。
练习 7:闭包的借用冲突与修法
难度:★★☆
要求(修借用冲突):下面代码编译不过,给出两种不同的修法并各自说明适用场景。
rust
fn main() {
let mut nums = vec![1, 2, 3, 4];
let limit = 3;
nums.retain(|n| {
if *n > limit {
nums.push(*n); // 把超限的挪到末尾
}
*n <= limit
});
println!("{nums:?}");
}提示:一种是把要改的集合先 mem::take 出来;另一种是先算出「保留哪些」再统一重建。
参考答案(先自己写再看)
参考答案(先自己写再看)
原代码报错:
error[E0499]: cannot borrow `nums` as mutable more than once at a time因为 retain 已经可变借用了 nums,闭包里又 nums.push。
修法一:mem::take 把集合换出来(适合「边遍历边追加」的语义)
rust
fn main() {
let mut nums = vec![1, 2, 3, 4];
let limit = 3;
let mut taken = std::mem::take(&mut nums); // nums 变成空 Vec
taken.retain(|n| *n <= limit); // 现在 taken 可以自由使用
nums = taken; // 放回
println!("{nums:?}");
}修法二:先算决定,再统一重建(推荐,语义最清晰)
rust
fn main() {
let mut nums = vec![1, 2, 3, 4];
let limit = 3;
let (keep, moved): (Vec<i32>, Vec<i32>) = nums.iter().partition(|n| **n <= limit);
nums = keep; // 借用已结束才能赋值
nums.extend(moved);
println!("{nums:?}");
}两种修法的输出都是:
[1, 2, 3, 4](超限元素被挪到末尾,但因为只有一个超限元素,看起来一样。把输入换成 [5, 1, 4, 2]、limit = 3 会更明显:修法一得到 [1, 2, 5, 4], 修法二得到 [1, 2, 5, 4]。)
要点解析:
- 修法一适合「原地保留 + 沿途副作用」的场景,优点是零额外结构; 注意
mem::take之后原变量是空Vec,中途 panic 会留下不完整状态。 - 修法二适合「决策纯粹依赖元素」的场景,
partition一次扫描、结果清晰, 而且没有任何时刻处于「半空」状态,更容易推理。 - 反例(不要这么做):
let v = std::mem::take(&mut nums); ...; nums = v;中间又调用nums——那说明设计上真的需要两个容器,应该显式声明两个变量而不是来回搬。
练习 8:Rc / Arc 跨线程的编译判断
难度:★★☆
要求(判断能否编译):下面 4 段代码哪些能编译?为什么?逐条给出结论。 (每段用独立模块包起来,「能否编译」指的是单独把这一段拿出来编译。)
rust
mod a {
use std::rc::Rc;
use std::thread;
pub fn run() {
let data = Rc::new(vec![1, 2, 3]);
thread::spawn(move || println!("{:?}", data));
}
}
mod b {
use std::sync::{Arc, Mutex};
use std::thread;
pub fn run() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
thread::spawn(move || data.lock().unwrap().push(4));
println!("{:?}", data.lock().unwrap());
}
}
mod c {
use std::sync::{Arc, Mutex};
use std::thread;
pub fn run() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let d = Arc::clone(&data);
thread::spawn(move || d.lock().unwrap().push(4));
println!("{:?}", data.lock().unwrap());
}
}
mod d {
use std::cell::RefCell;
use std::sync::Arc;
use std::thread;
pub fn run() {
let data = Arc::new(RefCell::new(vec![1, 2, 3]));
thread::spawn(move || data.borrow_mut().push(4));
}
}
fn main() {}提示:两个问题——这个闭包 Send 吗?里面的数据 Sync 吗?
参考答案(先自己写再看)
参考答案(先自己写再看)
| 代码 | 能编译? | 错误码 | 原因 |
|---|---|---|---|
| A | ❌ | E0277 | Rc<Vec<i32>> 不是 Send,不能移到新线程 |
| B | ❌ | E0502 | 闭包移动了 data,之后主线程再用 data 就是借用已移动的值 |
| C | ✅ | — | Arc::clone 给线程一份,主线程保留自己的那份 |
| D | ❌ | E0277 | RefCell<Vec<i32>> 不是 Sync,Arc<RefCell<..>> 因此不是 Send |
A 的报错原文:
error[E0277]: `Rc<Vec<i32>>` cannot be sent between threads safely
--> e0277_send.rs:6:37
|
6 | let handle = std::thread::spawn(move || {
| ------------------ ^------
| |__________________|__________________within this `{closure@...}`
|
= help: within `{closure@...}`, the trait `Send` is not implemented for
`Rc<Vec<i32>>`
note: required because it's used within this closure
note: required by a bound in `spawn`B 的报错形态(E0502):thread::spawn 内部把 data 移动了(move 闭包), data.lock() 又要求借用它。
rust
use std::sync::{Arc, Mutex};
use std::thread;
pub fn run() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
// 修法:先克隆一份给线程(加计数),主线程保留自己的那份
let for_thread = Arc::clone(&data);
thread::spawn(move || for_thread.lock().unwrap().push(4));
println!("{:?}", data.lock().unwrap());
}
fn main() {
run();
}D 的修法:把 RefCell 换成 Mutex(Mutex 是 Sync,RefCell 不是):
rust
use std::sync::{Arc, Mutex};
use std::thread;
pub fn run() {
// 唯一的改动:RefCell -> Mutex,borrow_mut() -> lock().unwrap()
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let d = Arc::clone(&data);
thread::spawn(move || d.lock().unwrap().push(4));
println!("{:?}", data.lock().unwrap());
}
fn main() {
run();
}要点解析(判断这类问题的两步法):
- 闭包本身
Send吗? 闭包Send当且仅当它捕获的每个变量都Send。Rc永远!Send→ A 失败;Arc<T>在T: Send + Sync时Send。 Arc里的数据Sync吗?Arc<T>: Send要求T: Send + Sync。Mutex<T>: Sync(当T: Send)→ C 通过;RefCell<T>永远!Sync→ D 失败。- 别忘了「移动后不能再用」这条普通规则:B 与线程安全无关,纯粹是所有权问题。
练习 9:返回 impl Fn 的闭包工厂
难度:★★☆
要求:写一个函数 build_greeter(prefix: String) -> impl Fn(&str) -> String, 返回的闭包把 prefix 和参数拼起来。再写一个 make_bounded_counter(limit: u32) -> impl FnMut() -> Option<u32>, 每次调用返回下一个值,超过 limit 后返回 None。说明为什么前者是 Fn、后者是 FnMut。
提示:返回值使用闭包时,捕获局部变量必须 move。
参考答案(先自己写再看)
参考答案(先自己写再看)
rust
fn build_greeter(prefix: String) -> impl Fn(&str) -> String {
// move:prefix 被移进闭包,否则函数返回后它就没了
move |name| format!("{prefix}, {name}!")
}
fn make_bounded_counter(limit: u32) -> impl FnMut() -> Option<u32> {
let mut current = 0;
move || {
if current >= limit {
return None;
}
current += 1;
Some(current)
}
}
fn main() {
let greet = build_greeter(String::from("你好"));
println!("{}", greet("Rust")); // Fn:可以反复调用,不需要 mut
println!("{}", greet("世界"));
let mut counter = make_bounded_counter(3); // FnMut:绑定必须是 mut
println!("{:?}", counter());
println!("{:?}", counter());
println!("{:?}", counter());
println!("{:?}", counter()); // 超过 limit -> None
}输出:
你好, Rust!
你好, 世界!
Some(1)
Some(2)
Some(3)
None要点解析:
build_greeter的闭包只读捕获的prefix(format!借用它),所以实现Fn。 调用方拿到的是Fn,不需要let mut greet。make_bounded_counter的闭包要current += 1(可变借用捕获变量),所以是FnMut, 调用方必须写let mut counter = ...,否则报E0596。- 两个函数都必须
move:闭包要活过函数调用,而prefix/current是局部变量。 不加move会报E0373 closure may outlive the current function。 - 想再放宽成
FnOnce(比如返回时把current交出去)就写impl FnOnce() -> u32。
练习 10:用 Rc<RefCell> 与 Weak 建图遍历
难度:★★★
要求:用 Rc<RefCell<Node>> + Weak 实现一个有向图:
Node拥有三个字段:id: u32edges: RefCell<Vec<Rc<RefCell<Node>>>>—— 出边,用强引用(我拥有我的邻居)incoming: RefCell<Vec<Weak<RefCell<Node>>>>—— 入边,用弱引用(避免成环)
- 提供
link(from, to):from.edges存Rc,to.incoming存Weak; - 提供一个
reachable_from(start) -> Vec<u32>做 BFS(注意每步borrow的作用域要短, 避免同时持有两个节点的RefMut); - 在
main里构造a -> b -> c,打印reachable_from(&a),并打印各节点计数。
提示:BFS 时不要持有 RefMut 遍历——要么先 clone 出邻接表,要么在最小作用域里取完就放。
参考答案(先自己写再看)
参考答案(先自己写再看)
rust
use std::cell::RefCell;
use std::collections::{HashSet, VecDeque};
use std::rc::{Rc, Weak};
type NodeRef = Rc<RefCell<Node>>;
#[derive(Debug)]
struct Node {
id: u32,
edges: RefCell<Vec<NodeRef>>, // 出边:强引用(我拥有我的邻居)
incoming: RefCell<Vec<Weak<RefCell<Node>>>>, // 入边:弱引用(避免成环)
}
fn new_node(id: u32) -> NodeRef {
Rc::new(RefCell::new(Node {
id,
edges: RefCell::new(Vec::new()),
incoming: RefCell::new(Vec::new()),
}))
}
fn link(from: &NodeRef, to: &NodeRef) {
from.borrow().edges.borrow_mut().push(Rc::clone(to));
to.borrow().incoming.borrow_mut().push(Rc::downgrade(from));
}
fn reachable_from(start: &NodeRef) -> Vec<u32> {
let mut seen: HashSet<u32> = HashSet::new();
let mut queue: VecDeque<NodeRef> = VecDeque::new();
let mut order: Vec<u32> = Vec::new();
seen.insert(start.borrow().id);
queue.push_back(Rc::clone(start));
while let Some(node) = queue.pop_front() {
let id = node.borrow().id;
order.push(id);
// 关键:把邻接表 clone 出来,立刻结束 Ref,避免后面 borrow_mut 冲突
let neighbors: Vec<NodeRef> = node.borrow().edges.borrow().clone();
for n in neighbors {
let nid = n.borrow().id;
if seen.insert(nid) {
queue.push_back(n);
}
}
}
order
}
fn main() {
let a = new_node(1);
let b = new_node(2);
let c = new_node(3);
link(&a, &b);
link(&b, &c);
link(&a, &c); // a 直接也能到 c
println!("从 a 可达: {:?}", reachable_from(&a));
println!("从 b 可达: {:?}", reachable_from(&b));
for (name, n) in [("a", &a), ("b", &b), ("c", &c)] {
println!(
"{name}(id={}): strong={}, weak={}",
n.borrow().id,
Rc::strong_count(n),
Rc::weak_count(n)
);
}
// 反向导航:用 Weak 回望入边
let incoming_of_c: Vec<u32> = c
.borrow()
.incoming
.borrow()
.iter()
.filter_map(|w| w.upgrade())
.map(|n| n.borrow().id)
.collect();
println!("指向 c 的节点: {incoming_of_c:?}");
}输出:
从 a 可达: [1, 2, 3]
从 b 可达: [2, 3]
a(id=1): strong=1, weak=2
b(id=2): strong=2, weak=1
c(id=3): strong=3, weak=0
指向 c 的节点: [2, 1]要点解析:
link里from.borrow().edges.borrow_mut().push(...)是安全的:先Ref再RefMut,但两者是不同RefCell的借用,不会冲突。若写成from.borrow_mut().edges.borrow_mut()也合法(都是同一节点的不同字段)。- 计数解读:
c的strong=3= 局部变量c+b.edges+a.edges;b的strong=2= 局部变量b+a.edges;a的strong=1= 只有局部变量。weak计数验证了incoming用的是弱引用:b的weak=1(来自a的入边),a的weak=2(来自b和c的入边),c的weak=0(没有入边指向它)。 指向 c 的节点: [2, 1]的顺序来自link的调用顺序(先b -> c,后a -> c), 而不是 BFS 顺序——incoming是个Vec,追加顺序就是调用顺序。- BFS 里最关键的防 panic 技巧:先把邻接表
clone成局部Vec<NodeRef>, 让node的Ref立刻结束,之后处理邻居时再借用其他节点就不会互相打架。 如果写成for n in node.borrow().edges.borrow().iter() { ... n.borrow_mut() ... }, 当图里有自环(a -> a)时会直接BorrowMutError。 - 出边用强引用意味着「一个节点活着就保持它的邻居活着」;如果想让图能被部分回收, 出边也应该用
Weak,代价是遍历时要处理upgrade()返回None的情况。
练习 11:FnMut 维护状态与 fold 组合
难度:★★☆
要求(FnMut 与迭代器):实现 fn running_max(values: &[i32]) -> Vec<i32>, 返回「到目前为止的最大值」序列(闭包用 FnMut 维护状态)。再用 fold 实现 fn longest_prefix_under(words: &[&str], limit: usize) -> usize, 返回最后一个长度不超过 limit 的单词的下标 + 1(找不到返回 0)。
提示:fold 的累加器可以携带 (索引, 结果) 这样的复合状态。
参考答案(先自己写再看)
参考答案(先自己写再看)
rust
fn running_max(values: &[i32]) -> Vec<i32> {
let mut best = i32::MIN; // 闭包捕获 &mut best -> FnMut
values
.iter()
.map(|v| {
if *v > best {
best = *v;
}
best
})
.collect()
}
fn longest_prefix_under(words: &[&str], limit: usize) -> usize {
// 累加器是 (索引, 结果):索引从 1 开始,方便直接返回长度
let (_, result) = words.iter().enumerate().fold((0usize, 0usize), |(idx, acc), (i, w)| {
let next_idx = i + 1;
if w.len() <= limit {
(next_idx, next_idx) // 这个单词合格,结果推进到它
} else {
(next_idx, acc) // 不合格就保持旧结果
}
});
result
}
fn main() {
println!("{:?}", running_max(&[3, 1, 4, 1, 5, 9, 2, 6]));
println!("{:?}", running_max(&[-5, -1, -9]));
let words = ["hi", "rustacean", "ferris", "ok", "extraordinarily"];
println!("limit=4 -> {}", longest_prefix_under(&words, 4)); // 到 "ok" 为止
println!("limit=2 -> {}", longest_prefix_under(&words, 2)); // 只有 "hi"
println!("limit=99 -> {}", longest_prefix_under(&words, 99)); // 全部
println!("limit=0 -> {}", longest_prefix_under(&words, 0)); // 一个都没有
}输出:
[3, 3, 4, 4, 5, 9, 9, 9]
[-5, -1, -1]
limit=4 -> 4
limit=2 -> 1
limit=99 -> 5
limit=0 -> 0要点解析:
map的闭包约束是FnMut,所以允许在里面改捕获的best。如果map要求Fn(&self调用),best = *v就会编译失败。fold的闭包同样是FnMut,累加器(idx, acc)就是「跨元素携带的状态」。 用元组比用两个可变外部变量更清晰,也不会引入借用冲突。- 注意
i32::MIN作为初值:如果写0,running_max(&[-5, -1])会得到[0, 0], 这是这类题最经典的边界错误。