跳转至

10. 智能指针篇

对应章节:第15章(智能指针)。
本篇掌握 Rust 所有权模型与堆内存管理:Box / Rc / RefCell / Weak。


项目 17:双向链表

目标:用 Box / Rc / RefCell / Weak 实现不同层次的链表,理解所有权模型。

创建项目

cargo new linked_list
cd linked_list

功能要求(三阶段)

  • 阶段A:单链表(Box)——push_front / push_back / len / print
  • 阶段B:共享尾部(Rc)——观察 strong_count
  • 阶段C:双向链表(Rc<RefCell> + Weak)——push_front / pop_front / print,防循环引用

知识点

知识点 说明
Box<T> 堆分配、单所有权
mem::replace 取出旧值放默认值
while let 模式匹配循环
Rc<T> 引用计数、共享所有权
RefCell<T> 内部可变性、运行时借用检查
Weak<T> 弱引用,防循环
Option::take() 取走值留 None
NLL 非词法生命周期

阶段A:单链表(Box)

src/main.rs:

use std::mem;

// 链表节点:要么是空(Nil),要么是"值 + 指向下一个节点的 Box"
// Box 是堆分配:Node 大小不定,用 Box 固定大小可递归定义
enum Node {
    Nil,
    Cons(i32, Box<Node>),   // Box 持有一个 Node(唯一所有权)
}

struct LinkedList {
    head: Node,
}

impl LinkedList {
    fn new() -> LinkedList {
        LinkedList { head: Node::Nil }
    }

    // 从头部插入:mem::replace 取出旧头,换成新头
    fn push_front(&mut self, value: i32) {
        // mem::replace 把 self.head 替换成 Nil,并返回旧值
        // 绕过"不能从借用中 move 出值"的限制
        let old_head = mem::replace(&mut self.head, Node::Nil);
        self.head = Node::Cons(value, Box::new(old_head));   // 旧头变成新头的 next
    }

    // 从尾部插入:while let 走到最后一个节点
    fn push_back(&mut self, value: i32) {
        let mut current = &mut self.head;
        // while let:只要 current 是 Cons 就继续往前走
        // next 是 &mut Box<Node>,可以再次模式匹配(NLL 允许)
        while let Node::Cons(_, next) = current {
            current = next;   // 走到下一个节点
        }
        // 现在 current 指向 Nil,把它替换成新节点
        *current = Node::Cons(value, Box::new(Node::Nil));
    }

    fn len(&self) -> usize {
        let mut count = 0;
        let mut current = &self.head;
        while let Node::Cons(_, next) = current {
            count += 1;
            current = next;
        }
        count
    }

    fn print(&self) {
        let mut current = &self.head;
        while let Node::Cons(v, next) = current {
            print!("{} ", v);
            current = next;
        }
        println!();
    }
}

fn main() {
    let mut list = LinkedList::new();
    list.push_front(1);
    list.push_front(2);
    list.push_back(3);
    list.print();          // 2 1 3
    println!("长度:{}", list.len());   // 3
}

阶段B:Rc 共享尾部

use std::rc::Rc;

// 用 Rc 代替 Box:允许多个节点共享同一个尾部
// Rc = Reference Counting:每多一个持有者计数 +1,计数归零才释放
enum SharedNode {
    Nil,
    Cons(i32, Rc<SharedNode>),
}

fn main() {
    // tail 被两个链表共享
    let tail = Rc::new(SharedNode::Cons(3, Rc::new(SharedNode::Nil)));

    // Rc::clone 不是深拷贝!只是计数 +1,数据只有一份
    let list1 = SharedNode::Cons(1, Rc::clone(&tail));
    let list2 = SharedNode::Cons(2, Rc::clone(&tail));

    // tail 被 list1 和 list2 各持有一份引用 → 计数为 2
    println!("tail 强引用计数:{}", Rc::strong_count(&tail));  // 2

    // 如果换成 Box:tail 被移进 list1 后 list2 就无法再用了
    // (Box 是单所有权,不能有第二个持有者)
}

阶段C:双向链表(Rc + Weak)

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

struct Node {
    value: i32,
    // 前→后:强引用(计数)。Rc 让多个节点能共享指向同一个后续节点
    next: Option<Rc<RefCell<Node>>>,
    // 后→前:弱引用(不计数)。Weak 不增加计数,防止循环引用
    prev: Option<Weak<RefCell<Node>>>,
}

impl Node {
    fn new(value: i32) -> Rc<RefCell<Self>> {
        // Rc 提供共享,RefCell 提供内部可变性(borrow_mut 修改字段)
        Rc::new(RefCell::new(Self {
            value,
            next: None,
            prev: None,
        }))
    }
}

struct DoublyList {
    head: Option<Rc<RefCell<Node>>>,
    tail: Option<Rc<RefCell<Node>>>,
}

impl DoublyList {
    fn new() -> Self {
        Self {
            head: None,
            tail: None,
        }
    }

    fn push_front(&mut self, value: i32) {
        let new_node = Node::new(value);
        match &self.head {
            Some(old_head) => {
                // 新节点的 next 指向旧头(强引用)
                new_node.borrow_mut().next = Some(Rc::clone(old_head));
                // 旧头的 prev 指向新节点(弱引用,不计数)
                old_head.borrow_mut().prev = Some(Rc::downgrade(&new_node));
                self.head = Some(new_node);
            }
            None => {
                // 空链表:head 和 tail 都指向新节点
                self.head = Some(Rc::clone(&new_node));
                self.tail = Some(new_node);
            }
        }
    }

    fn pop_front(&mut self) -> Option<i32> {
        // take() 拿走 head 留 None;? 在 Option 上遇到 None 直接返回
        let head = self.head.take()?;
        match head.borrow_mut().next.take() {
            Some(next) => {
                // 新头不再指旧头(旧头即将释放)
                next.borrow_mut().prev = None;
                self.head = Some(next);
            }
            None => {
                // 删完链表空了
                self.head = None;
                self.tail = None;
            }
        }
        Some(head.borrow_mut().value)   // borrow 只读访问 value
    }

    fn print(&self) {
        let mut cur = self.head.clone();
        while let Some(node) = cur {
            print!("{}", node.borrow().value);
            cur = node.borrow().next.clone();   // clone Rc 推进循环
        }
        println!();
    }
}

fn main() {
    let mut list = DoublyList::new();
    list.push_front(1);
    list.push_front(2);
    list.push_front(3);
    list.print();                                  // 321
    println!("{:?}", list.pop_front().unwrap());   // 3
    list.print();                                  // 21

    // Weak 悬垂演示:弱引用不阻止数据被释放
    let a = Rc::new(10);
    let weak = Rc::downgrade(&a);
    println!("{:?}", weak.upgrade());   // Some(10):强引用还活着

    drop(a);   // 手动释放最后一个强引用
    println!("{:?}", weak.upgrade());   // None:upgrade 不会复活已释放的数据
}

运行与验证

# 阶段C
cargo run
# 321
# 3
# 21
# Some(10)
# None

# 阶段B 观察引用计数
cargo run
# tail 强引用计数:2

注意事项(重点)

  1. Box 是单所有权:tail 移进 list1 后,list2 无法使用(use of moved value)。Box::clone 是深拷贝,不是共享。
  2. Rc::clone 只加计数:数据只有一份,多个 Rc 指向它;strong_count 归零才释放。Rc 仅单线程可用(非原子计数)。
  3. Rc 里值不可变:Rc<T> 只能拿 &T,要修改必须套 RefCell<T>(内部可变性,运行时检查借用,违反会 panic)。
  4. 循环引用 = 内存泄漏:双向链表若 prev 也用 Rc,A↔B 互相强持有,计数永远 ≥1 永不释放。prev 用 Weak(不计数)打破循环。Rc::downgrade 建弱引用,Weak::upgrade 升回强引用(可能 None)。main 末尾的悬垂演示值得跑一遍:drop(a) 后 upgrade() 返回 None——弱引用眼睁睁看着数据释放,这正是"不阻止析构"的含义。
  5. Option::take():从 &mut Option 中移出值留 None,是"拿走旧状态"的标准手段(第16、17章也用到)。
  6. mem::replace(&mut self.head, Node::Nil):先把 head 换成 Nil 取走旧值,再构造新头——绕开"不能 move 出借用"的限制。

评论