Skip to content

练习与自测

本章练习共 11 题,答案折叠在每题下方。建议先自己写、编译通过后再展开答案;难度标记:★☆☆ 基础 / ★★☆ 综合 / ★★★ 挑战。

练习 1:实现智能指针 MyBox

难度:★★☆

要求:实现泛型智能指针 MyBox<T>,要求:

  1. MyBox::new(x) 构造;
  2. 实现 Deref,使其能用于需要 &T 的地方(用 fn str_len(s: &str) -> usize 验证解引用强制转换);
  3. 实现 DerefMut,使 *boxed = new_value 能编译;
  4. 实现 Drop,析构时打印 释放 <值>,并解释为什么 Drop 里的字段仍然会被自动释放。

提示Deref 返回 &self.0DerefMut 返回 &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 只加计数,urgentitems 指向同一个 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),要求:

  1. prevWeaknextOption<Rc<..>>
  2. 提供 push_backprev_ofnext_of
  3. main 里打印 strong_count / weak_count,并在 drop 掉尾部节点后证明 无循环泄漏(给节点加 Drop 打印,或用计数说明)。

提示prevWeakdowngrade 不会增加强引用;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)

要点解析

  • midstrong=2:局部变量 mid 一份 + head.next 里一份。 tailstrong=2:局部变量 + mid.nexthead 只有 1 份(局部变量)。
  • headweak=1 来自 mid.prevmidweak=1 来自 tail.prevtailweak=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 的方式访问 cacheRefCell 把可变性藏在了共享引用后面),编译器因此把它判定为 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

要点解析

  • 为什么不是 CellCell::get() 要求 T: CopyVec<Listener> 显然不是 Copy; 而且我们要在遍历时拿到元素的引用(&dyn Fn(&str))来调用,Cell 根本不允许 把内部值的引用借出去(只能整体 take/replace)。RefCell 提供 borrow() 返回 Ref<Vec<..>>,可以安全地遍历。
  • total_chars = 9 来自 "alpha"(5 个字符)+ "beta"(4 个字符)。
  • set 里先 clonesnapshot 是因为:如果在遍历 listenersRef 期间, 某个回调又调用 subscribe(要 borrow_mut),就会运行期 panic。这个「先快照再回调」 是观察者/事件系统里最常见的防 panic 写法。
  • Rc<dyn Fn(&str)> 是必需的:两个闭包是不同类型,只有 trait 对象能放进同一个 Vec

练习 6:CellRefCell 的选型判断

难度:★☆☆

要求(选型题):下面四个字段分别该用 Cell 还是 RefCell?说明理由。

  1. request_count: ???<u64>fn serve(&self) 里自增)
  2. log_buffer: ???<Vec<String>>fn log(&self, msg: &str) 里 push)
  3. is_dirty: ???<bool>fn render(&self) 里读、fn mark_dirty(&self) 里置 true)
  4. cache: ???<HashMap<String, Vec<u8>>>fn get_or_load(&self, k: &str) -> Vec<u8>

提示:问自己两个问题——「能不能整体复制?」和「需不需要拿到内部 &mut?」。

参考答案(先自己写再看)参考答案(先自己写再看)
字段选择理由
request_count: u64Cell<u64>u64: Copyget/set 各一条指令,零开销
log_buffer: Vec<String>RefCell<Vec<String>>需要在 &selfpush(要 &mut Vec);Vec 不是 CopyCellget 不可用
is_dirty: boolCell<bool>bool: Copy,语义就是「整体读/写」,没有借用内部的需求
cache: HashMap<String, Vec<u8>>RefCell<HashMap<..>>需要 get/entry 拿内部 &mut(或至少拿 & 长期存活),HashMap 也不 Copy

两个判断问题:

  1. 能不能整体复制? T: Copy 才有资格考虑 CellCell::get 的要求)。
  2. 需不需要内部值的引用? 只要需要 &T&mut T(遍历、pushentry、 传给别的函数),就必须 RefCellCell 的 API 刻意不提供借出内部引用的能力。

补充两点:

  • 如果 TVec<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 吗?

参考答案(先自己写再看)参考答案(先自己写再看)
代码能编译?错误码原因
AE0277Rc<Vec<i32>> 不是 Send,不能移到新线程
BE0502闭包移动了 data,之后主线程再用 data 就是借用已移动的值
CArc::clone 给线程一份,主线程保留自己的那份
DE0277RefCell<Vec<i32>> 不是 SyncArc<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 换成 MutexMutexSyncRefCell 不是):

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();
}

要点解析(判断这类问题的两步法):

  1. 闭包本身 Send 吗? 闭包 Send 当且仅当它捕获的每个变量都 SendRc 永远 !Send → A 失败;Arc<T>T: Send + SyncSend
  2. Arc 里的数据 Sync 吗? Arc<T>: Send 要求 T: Send + SyncMutex<T>: Sync(当 T: Send)→ C 通过;RefCell<T> 永远 !Sync → D 失败。
  3. 别忘了「移动后不能再用」这条普通规则: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 的闭包只读捕获的 prefixformat! 借用它),所以实现 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: u32
    • edges: RefCell<Vec<Rc<RefCell<Node>>>> —— 出边,用强引用(我拥有我的邻居)
    • incoming: RefCell<Vec<Weak<RefCell<Node>>>> —— 入边,用弱引用(避免成环)
  • 提供 link(from, to)from.edgesRcto.incomingWeak
  • 提供一个 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]

要点解析

  • linkfrom.borrow().edges.borrow_mut().push(...) 是安全的:先 RefRefMut,但两者是不同 RefCell 的借用,不会冲突。若写成 from.borrow_mut().edges.borrow_mut() 也合法(都是同一节点的不同字段)。
  • 计数解读:cstrong=3 = 局部变量 c + b.edges + a.edgesbstrong=2 = 局部变量 b + a.edgesastrong=1 = 只有局部变量。 weak 计数验证了 incoming 用的是弱引用:bweak=1(来自 a 的入边), aweak=2(来自 bc 的入边),cweak=0(没有入边指向它)。
  • 指向 c 的节点: [2, 1] 的顺序来自 link 的调用顺序(先 b -> c,后 a -> c), 而不是 BFS 顺序——incoming 是个 Vec,追加顺序就是调用顺序。
  • BFS 里最关键的防 panic 技巧:先把邻接表 clone 成局部 Vec<NodeRef>, 让 nodeRef 立刻结束,之后处理邻居时再借用其他节点就不会互相打架。 如果写成 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 作为初值:如果写 0running_max(&[-5, -1]) 会得到 [0, 0], 这是这类题最经典的边界错误。

本章小结 / 自测清单

内容以 rustc 1.98.1 · Rust 2024 edition 为基准