[rust]引用循环与弱引用

4 阅读2分钟

定义

如果两个或更多的 Rc(引用计数智能指针)实例互相持有对方或者通过其他对象间接组成一个循环引用,会导致内存泄漏。这是因为 Rc 通过引用计数来管理其底层数据的生命周期,而循环引用导致引用计数永远不会降为零,因此内存无法被释放

循环引用

#[derive(Debug)]
struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
}

use std::cell::RefCell;
fn main() {
    let first = Rc::new(RefCell::new(Node {
        value: 1,
        next: None,
    }));

    let second = Rc::new(RefCell::new(Node {
        value: 2,
        next: Some(Rc::clone(&first)),
    }));

    {
        let mut first_borrowed = first.borrow_mut();
        first_borrowed.next = Some(Rc::clone(&second)); // 形成引用循环
    }

    // 在这个点上,两个 Rc 的引用计数都不可能为 0,内存将永远不会释放
    println!("first count: {}", Rc::strong_count(&first)); // 2
    println!("second count: {}", Rc::strong_count(&second)); // 2
    println!("这里打印会无限循环,最终报错: fatal runtime error: stack overflow {:?}", first)
}

解决循环引用

一种常见解决方案是使用 RcWeak 智能指针的组合。WeakRc 的弱引用版本,它不增加引用计数,从而打破循环引用

#[derive(Debug)]
struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
    prev: Option<Weak<RefCell<Node>>>, // 使用 Weak 引用打破循环
}

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

fn main() {
    // 创建第一个节点
    let first = Rc::new(RefCell::new(Node {
        value: 1,
        next: None,
        prev: None,
    }));

    // 创建第二个节点,并让它的 prev 指向第一个节点
    let second = Rc::new(RefCell::new(Node {
        value: 2,
        next: None,
        prev: Some(Rc::downgrade(&first)), // 使用 Weak 指针 [1 <- 2]
    }));

    // 更新第一个节点的 next 指向第二个节点 [1 -> 2]
    first.borrow_mut().next = Some(Rc::clone(&second));

    // 打印引用计数
    println!("first strong count: {}", Rc::strong_count(&first)); // 打印1: 只有自己
    println!("second strong count: {}", Rc::strong_count(&second)); // 打印2:自己的引用 + 1.next 指向自己的引用

    // 打印第二个节点的 prev 是否存在
    {
        let second_prev = first.borrow().next.as_ref().unwrap().borrow().prev.as_ref().unwrap().upgrade();
        println!("Second node's previous exists: {:?}", second_prev.is_some());
    }

    // 程序结束时,引用会被正确释放
}

关键说明:

  1. Rc::downgrade: 获取指向 firstWeak 引用,而不是 Rc 引用。Weak 引用不会增加引用计数,这样即使 second.prev 持有 first 的相应引用,也不会导致循环引用
  2. Weak::upgrade: 在需要使用时,将 Weak 引用升级为 Option<T>。如果 Rc 仍然存在,则 upgrade 会返回 Some,否则返回 None