Weak 弱引用:如何用 Weak 打破 Rc 与 Arc 的循环引用

Weak 弱引用:如何用 Weak 打破 Rc 与 Arc 的循环引用

在很多人的认知里,Rust ≈ 没有内存问题。但如果你写过稍微复杂一点的数据结构,你很快就会遇到一个现实:Rust 不会产生悬垂指针,但*仍然可能发生内存泄漏。而这类泄漏,往往就来自一个经典陷阱:Rc/Arc 的循环引用。

Rust 为什么也会内存泄漏?

Rust 的内存安全,主要解决的是:悬垂指针、double free 等,但它并不保证一定释放内存

来看一个真实例子:

rust 复制代码
use std::rc::Rc;

fn main() {
    let a = Rc::new(1);
    let b = Rc::clone(&a);

    println!("{}", Rc::strong_count(&a)); // 2
}

只要 strong_count 不为零,数据就不会被释放。也就是说这就会造成内存泄漏。

循环引用是如何产生的?

要理解循环引用的产生,我们先回顾一个核心前提:Rc 与 Arc 的强引用计数规则是只要 strong_count 不为零,数据就不会被释放,且每一次强引用克隆都会让计数加一。

循环引用的本质,就是多个 Rc 与 Arc 实例互相持有对方的强引用,形成一个闭环,导致彼此的 strong_count 永远无法归零。

来看一个经典错误设计,双向链表,如下所示:

rust 复制代码
use std::cell::RefCell;
use std::rc::Rc;

// 错误设计:next和prev均为强引用
struct Node {
    next: Option<Rc<RefCell<Node>>>, // 强引用持有下一个节点
    prev: Option<Rc<RefCell<Node>>>, // 强引用持有上一个节点
}

fn main() {
    // 创建节点A和B,初始时next和prev均为None
    let a = Rc::new(RefCell::new(Node {
        next: None,
        prev: None,
    }));
    let b = Rc::new(RefCell::new(Node {
        next: None,
        prev: None,
    }));

    // 让A的next指向B,B的prev指向A
    a.borrow_mut().next = Some(Rc::clone(&b)); // A强引用B,b的strong_count +=1(变为2)
    b.borrow_mut().prev = Some(Rc::clone(&a)); // B强引用A,a的strong_count +=1(变为2)
}

在上面的示例中,我们让 A、B 两个节点互相强引用,形成了闭环。此时,两个节点的强引用计数状态 strong_count 均为2。

接下来,我们尝试释放外部持有的引用:

rust 复制代码
drop(a); // 释放外部对A的引用,A的strong_count变为1(仅剩B的prev持有)
drop(b); // 释放外部对B的引用,B的strong_count变为1(仅剩A的next持有)

此时最关键的问题出现了:虽然外部引用已经全部释放,但节点A和B仍然互相持有对方的强引用,即 A 的 next 持有 B,B 的 prev 持有 A。

因此,这两个节点占用的内存永远不会被 Rust 释放,这就是循环引用导致的逻辑内存泄漏

为什么 Rust 不自动解决循环引用?

一个很自然的问题是:Rust 为什么不帮我检测并回收这些环?答案很简单,那就是代价太高,而且违背设计哲学

Rc 和 Arc 只知道有多少人引用我,但不知道整个对象图长什么样。如果想检测有没有环就需要全局扫描,这其实就是垃圾回收(GC)在做的事情。

Rust 的哲学是用类型系统表达所有权,而不是运行时猜测。所以绝不会引入 GC,更不会去自动检测循环,这一切的工作交给开发者。

用 Weak 打破循环引用

为了解决这个问题,Rust 提供了 Weak,它的特性如下:

  • 不影响强引用计数:创建 Weak 实例时,不会增加目标数据的 strong_count,只影响 weak_count(弱引用计数),weak_count 不决定数据是否释放。
  • 不拥有数据所有权:Weak 本身不"拥有"目标数据,哪怕存在多个 Weak 引用,只要目标数据的 strong_count 归零,数据就会被释放,不受 Weak 引用数量影响。
  • 无法直接访问数据:正因为不拥有所有权,Weak 不能直接 deref 访问数据(避免悬垂指针风险),必须通过 upgrade() 方法尝试将其转为强引用并访问数据。

我们用一段简单的代码,直观感受 Weak 的核用法,理解它如何体现"观察而非拥有":

rust 复制代码
use std::rc::Rc;

fn main() {
    // 创建强引用,此时 strong_count = 1,weak_count = 0
    let rc = Rc::new("hello world".to_string());

    // 将强引用降级为弱引用,此时 strong_count 仍为 1,weak_count = 1
    let weak = Rc::downgrade(&rc);

    // 尝试将弱引用升级为强引用:此时数据存在,升级成功
    if let Some(strong) = weak.upgrade() {
        println!("数据存在:{}", strong); // 输出:数据存在:hello world
    } else {
        println!("数据已释放");
    }

    // 释放强引用,此时 strong_count = 0,数据被释放
    drop(rc);

    // 再次尝试升级弱引用:数据已释放,升级失败
    if let Some(strong) = weak.upgrade() {
        println!("数据存在:{}", strong);
    } else {
        println!("数据已释放"); // 输出:数据已释放
    }
}

从这段代码能清晰看到 Weak 的设计逻辑:它就像一个"观察者",只能观察数据是否存在,却不能阻止数据被释放。这种"不拥有"的特性,正是打破 Rc 与 Arc 循环引用的关键。只要将循环中的一个强引用替换为 Weak 弱引用,"互相拥有"的闭环就会被打破,强引用计数就能正常归零。

我们来改造刚才的双向链表,这里的改造逻辑是:父对子用 Rc(强引用),而子对父用 Weak(弱引用),既保证父节点释放时子节点同步释放,也避免子节点持有父节点导致的循环引用。

rust 复制代码
use std::cell::RefCell;
use std::rc::{Rc, Weak};

// 正确设计:next 强引用,prev 弱引用(避免循环引用)
struct Node {
    next: Option<Rc<RefCell<Node>>>,   // 强引用持有下一个节点
    prev: Option<Weak<RefCell<Node>>>, // 弱引用持有上一个节点
}

fn main() {
    // 创建节点A和B,初始时next和prev均为None
    let a = Rc::new(RefCell::new(Node {
        next: None,
        prev: None,
    }));
    let b = Rc::new(RefCell::new(Node {
        next: None,
        prev: None,
    }));

    // 让A的next指向B, A强引用B
    a.borrow_mut().next = Some(Rc::clone(&b));

    // B的prev指向A,B弱引用A
    // Rc::downgrade将Rc强引用&a转换为弱引用&a
    b.borrow_mut().prev = Some(Rc::downgrade(&a));
}

Weak 的常见坑

Weak 虽能解决循环引用问题,但并非万能工具,若使用不当,反而会引入新的内存安全隐患或逻辑错误。

强行 unwrap,引发 panic

这是最常见、最容易忽略的坑。upgrade() 方法返回 Option,有些开发者图省事,直接用 unwrap() 强制获取升级后的强引用,一旦数据已释放,就会触发程序 panic。

在实际开发中,必须用 if letmatch 语句处理 upgrade() 的返回值,严禁 unwrap。

滥用 Weak,导致数据提前释放

有些开发者会过度使用 Weak,甚至将所有引用都改为 Weak,导致数据因 strong_count 为零被提前释放,引发逻辑错误。错误示例如下:

rust 复制代码
// 错误示例:主数据也用 Weak,导致数据提前释放

// 错误:没有强引用持有数据,strong_count = 0
let data = Weak::new(); 
let cache = RefCell::new(LruCache::new());
cache.borrow_mut().insert("key1".to_string(), &data);

// 此时 data 已被释放,缓存中无法获取到任何数据
println!("缓存获取:{:?}", cache.borrow().get("key1")); // 输出:None

这里只需要记住:拥有关系用 Rc/Arc,非拥有关系用 Weak。Weak 无法维持数据的生命周期,必须有至少一个强引用作为数据的拥有者。

忽视 weak_count,误以为 Weak 不影响内存

很多开发者认为 Weak 不影响数据释放,所以可以随意创建,但忽略了 Weak 会增加 weak_count。虽然 weak_count 不决定数据是否释放,但会影响 Rc/Arc 内部元数据(计数结构体)的释放。若大量创建 Weak 且不清理,会导致元数据内存泄漏。

特别是在高频调用的代码中,反复创建 Weak 但不回收,导致 weak_count 持续增长,元数据占用的内存无法释放。

总结

综上,Weak 与 Rc/Arc 并非对立关系,而是互补关系。Rc/Arc 负责"拥有"数据、维持生命周期,Weak 负责"观察"数据、打破循环闭环。

相关推荐
iCxhust1 小时前
在 emu8086 中可以直接编译运行的完整汇编程序,演示数组的定义、遍历、求和、求最大值。
开发语言·前端·javascript·汇编·单片机·嵌入式硬件·算法
贫民窟的勇敢爷们2 小时前
Spring Boot+Vue电商系统开发实战:架构设计与核心实现
vue.js·spring boot·后端
Brilliantwxx2 小时前
【C++】认识vector(概念+题目OJ)
开发语言·c++·笔记·算法
逻辑驱动的ken2 小时前
Java高频面试考点场景题22
java·开发语言·jvm·面试·职场和发展·求职招聘·春招
枫叶丹42 小时前
【HarmonyOS 6.0】Core File Kit:端云文件版本管理能力解析与实践
开发语言·华为·harmonyos
初心未改HD2 小时前
Go 文件与 I/O 操作完全指南
开发语言·golang
szial2 小时前
uv 实战指南:用一个工具重塑 Python 开发工作流
开发语言·python·uv
DogDaoDao2 小时前
【GitHub】Warp 终端深度解析:Rust + GPU 加速的 AI 原生终端开源架构
人工智能·程序员·rust·开源·github·ai编程·warp
wjs20242 小时前
HTML 段落
开发语言