首页
/ Rust E0524 错误深入解析:两个闭包争夺同一个 &mut 唯一借用的成因与解法

Rust E0524 错误深入解析:两个闭包争夺同一个 &mut 唯一借用的成因与解法

2026-09-07 14:36:32作者:乔或婵

本文围绕 Rust 官方错误码文档 E0524.md 展开,完整讲解 E0524(“同一变量被要求唯一访问,却同时被两个闭包捕获”)的触发场景、借用检查器(borrowck)中该错误码的判定路径与诊断输出,以及官方案文中的两种标准修复方案:Rc/Arc 共享所有权与闭包顺序执行。读完后你将能够:独立诊断含 E0524 的编译错误、理解闭包按 &mut 捕获变量时的生命周期语义,并选择正确的重构手段让代码通过编译。

什么是 E0524

E0524 的官方定义只有一句话:某个需要唯一访问(&mut)的变量,被同时用于多个闭包中(A variable which requires unique access is being used in more than one closure at the same time)。

它发生在这样的情形:函数(或函数体)里存在一个 &mut T 参数或可变本地变量,而你先后构造了两个闭包,这两个闭包都**按可变引用捕获(capture by &mut)**了同一个变量。由于闭包一旦构造完成,其捕获的借用就存活到闭包本身被丢弃为止,两个闭包同时“持有”对同一变量的可变借用,违反了 Rust 的唯一性规则,因此借用检查器拒绝编译。

官方错误示例

以下代码即文档中的标准反例(标注 compile_fail,E0524 表示该代码必然产生 E0524):

fn set(x: &mut isize) {
    *x += 4;
}

fn dragoooon(x: &mut isize) {
    let mut c1 = || set(x);
    let mut c2 = || set(x); // error!

    c2();
    c1();
}

注意错误并非发生在调用 c1()/c2() 的那两行,而是发生在构造 c2 的那一行——第二个闭包按 &mut 捕获 x 时,第一个闭包 c1x 的捕获借用仍未结束(c1 直到函数体结束才被 drop),两次可变捕获在时间区间上重叠,冲突成立。

源码级成因:borrowck 如何判定“两个闭包唯一借用”

触发点:MutBorrowKind::ClosureCapture 的冲突组合

该错误的判定入口在借用检查器的冲突诊断模块 conflict_errors.rs。借用检查器在处理“新借用与已存在借用冲突”时,会对双方借用的 BorrowKind 做模式匹配,其中一个专门的分支是:

(
    BorrowKind::Mut { kind: MutBorrowKind::ClosureCapture },
    BorrowKind::Mut { kind: MutBorrowKind::ClosureCapture },
) => {
    first_borrow_desc = "first ";
    self.cannot_uniquely_borrow_by_two_closures(span, &desc_place, issued_span, None)
}

即:当冲突双方都是 BorrowKind::Mut { kind: MutBorrowKind::ClosureCapture }(闭包按可变引用捕获变量产生的借用)时,走 E0524 专属诊断路径。这与普通的双重 &mutMutBorrowKind::Default/TwoPhaseBorrow)走 cannot_mutably_borrow_multiply(E0499 一类)的路径是分开的,从源码结构看,编译器刻意把“闭包之间的唯一借用竞争”单列为一个更精确的错误码,以便给出针对性的错误文案与修复提示。

诊断输出:cannot_uniquely_borrow_by_two_closures

具体的错误文案与 span 标注生成在 borrowck_errors.rs 中:

pub(crate) fn cannot_uniquely_borrow_by_two_closures(
    &self,
    new_loan_span: Span,
    desc: &str,
    old_loan_span: Span,
    old_load_end_span: Option<Span>,
) -> Diag<'diag> {
    let mut err = struct_span_code_err!(
        self.dcx(),
        new_loan_span,
        E0524,
        "two closures require unique access to {} at the same time",
        desc,
    );
    ...
}

可以读出三点信息:

  1. 主文案固定为 two closures require unique access to <变量> at the same time(两个闭包同时要求对某变量进行唯一访问),主 span 落在后构造的那个闭包上(new_loan_span);
  2. 如果两次借用 span 相同,说明闭包是在循环的不同迭代中构造的,此时附加提示为 “closures are constructed here in different iterations of loop”,否则分别标注 “first closure is constructed here” 和 “second closure is constructed here”;
  3. 如果已知第一个闭包捕获借用的结束位置,还会追加 “borrow from first closure ends here” 标注,帮助开发者直观看到两次借用的生命周期为何重叠。

这个循环迭代变体在测试文件 closures-in-loops.stderr 中也有对应输出样本。

可复现的官方回归测试

该错误码在仓库中有对应的 borrowck 回归测试,可用于本地验证编译行为:

解决方案一:Rc(或 Arc)+ RefCell 共享所有权

何时选择这条路

官方文档给出的判断标准是:如果这个变量确实需要被多个闭包“同时”(即两个闭包同时存活期间)使用,就改用引用计数类型打破“捕获即独占”的结构。单线程场景用 Rc,跨线程并发场景用 Arc

完整可运行示例

这是文档中的标准修复写法:

use std::rc::Rc;
use std::cell::RefCell;

fn set(x: &mut isize) {
    *x += 4;
}

fn dragoooon(x: &mut isize) {
    let x = Rc::new(RefCell::new(x));
    let y = Rc::clone(&x);
    let mut c1 = || { let mut x2 = x.borrow_mut(); set(&mut x2); };
    let mut c2 = || { let mut x2 = y.borrow_mut(); set(&mut x2); }; // ok!

    c2();
    c1();
}

逐行拆解其原理:

步骤 代码 作用
包装 Rc::new(RefCell::new(x)) &mut isize 包进 RefCell(运行时可变借用)再包进 Rc(共享所有权)
克隆句柄 let y = Rc::clone(&x); 让两个闭包持有不同的 Rc 句柄,指向同一份数据
闭包捕获 c1 捕获 xc2 捕获 y 此时闭包捕获的是 Rc 的克隆(按 Copy 语义或共享引用),而不是对原 x&mut 捕获,两个闭包可以共存
运行时互斥 x.borrow_mut() / y.borrow_mut() 把编译期的“唯一借用”冲突推迟到运行时:谁调用 borrow_mut(),谁就临时获得排他访问权

需要特别记住的语义变化:RefCell::borrow_mut()运行时检查——如果两个闭包在嵌套调用的情况下同时持有 RefCell 的可变借用,程序会 panic(“already mutably borrowed”)。也就是说,该方案以运行时开销和潜在的 panic 风险换取了“两个闭包可同时存在”的灵活性;若运行时逻辑能保证互斥,这是最贴合原意的方案。

跨线程版本只需替换类型:RcArcRefCellMutex<T>(或 Arc<RefCell<T>> 在单线程内共享、跨线程传递的混合场景),闭包体结构不变。

解决方案二:闭包顺序执行(依赖 NLL)

如果变量并不需要被多个闭包同时使用——比如闭包是依次创建、依次调用、依次废弃的,那么最简单的修复是保证前一个闭包先被 drop:

fn set(x: &mut isize) {
    *x += 4;
}

fn dragoooon(x: &mut isize) {
    { // This block isn't necessary since non-lexical lifetimes, it's just to
      // make it more clear.
        let mut c1 = || set(&mut *x);
        c1();
    } // `c1` has been dropped here so we're free to use `x` again!
    let mut c2 = || set(&mut *x);
    c2();
}

要点说明:

  1. 闭包必须真正捕获变量:注意这里写的是 set(&mut *x) 而不是原错误示例里的 set(x)。当 x: &mut isize 时,set(&mut *x) 使闭包捕获的是对 *x 的可变借用;而原始错误示例中 set(x) 会移动/按唯一引用捕获 x 本身,触发 E0524。
  2. { } 在此并非必须:文档明确注释说明,得益于非词法生命周期(Non-Lexical Lifetimes, NLL),即使不显式开块,借用检查器也会把 c1 的捕获借用结束位置判定为 c1() 调用之后(c1 不再被使用处),后面再构造 c2 即无冲突。示例中保留该块只是为了让“c1 在此被 drop”这一事实在源码中更直观。
  3. 与方案一的权衡:该方案保持编译期静态检查、零运行时开销,但要求你能调整代码结构,使闭包的创建/调用顺序互不重叠。

选择建议与相关错误辨析

  • 两个闭包的生命周期必须重叠(例如分别存入结构体字段、传入不同长生命周期组件)→ 方案一(Rc/Arc + RefCell/Mutex);
  • 闭包只是先后使用,可以重排创建与调用顺序 → 方案二(顺序执行 + NLL),无额外运行时负担;
  • 若冲突一方是闭包捕获、另一方是普通的 &mut 借用(而非另一个闭包),借用检查器会走不同的分支,报出的是 E0500 而不是 E0524——从源码中 BorrowKind::Mut { kind: MutBorrowKind::ClosureCapture } 与其他借用类型的组合分支(见 conflict_errors.rs 中紧随其后的 (BorrowKind::Mut { kind: MutBorrowKind::ClosureCapture }, _) 分支,对应 cannot_uniquely_borrow_by_one_closure 生成 E0500)可以看出,两个错误码的边界正是“冲突双方是否都是闭包捕获”。

小结

E0524 的本质是闭包按 &mut 捕获变量所产生的两次唯一借用生命周期重叠:判定发生在 borrowck 的冲突诊断中 ClosureCapture × ClosureCapture 分支,文案与 span 标注由 cannot_uniquely_borrow_by_two_closures 生成。修复只有两个方向——要么用 Rc/Arc + RefCell 把独占借用改为共享所有权加运行时互斥,要么重排代码让两个闭包的生命周期在 NLL 下错开。两者取舍取决于“是否需要同时持有多个闭包”这一实际问题。

登录后查看全文
热门项目推荐
相关项目推荐