首页
/ Rust 错误码 E0521 深度解析:借用数据逃逸出闭包——闭包参数类型标注如何引入新生命周期

Rust 错误码 E0521 深度解析:借用数据逃逸出闭包——闭包参数类型标注如何引入新生命周期

2026-09-07 14:26:11作者:廉彬冶Miranda

E0521(borrowed data escapes outside of closure)是 rustc 借用检查器报出的一个典型生命周期错误:当闭包/函数的参数是一个引用,而这个引用被存进了生命周期超出当前作用体的变量时,就会触发该错误。本文以官方错误码文档 E0521.md 为核心,结合 rustc 借用检查器的诊断源码与仓库中的 UI 测试用例,完整讲清 E0521 的触发机制、"闭包参数标注一个类型就意味着声明一个新生命周期"这一关键规则,以及四种可复制的规避写法。

一、E0521 的最小复现与报错信息

官方错误码文档给出的最小错误示例是(标记为 compile_fail,E0521,即预期编译失败):

let mut list: Vec<&str> = Vec::new();

let _add = |el: &str| {
    list.push(el); // error: `el` escapes the closure body here
};

关键点在于:list 是闭包外部声明的 Vec<&str>,而闭包参数 el: &str 显式标注了引用类型。list.push(el) 试图把 el 存入一个生命周期比闭包参数更长的容器,借用检查器随即报出:

error[E0521]: borrowed data escapes outside of closure
  |
  | let _add = |el: &str| {
  |              -  `el` is a reference that is only valid in the closure body
  |     list.push(el);
  |                ^^^^ `el` escapes the closure body here
  |
  |     ----- `list` declared here, outside of the closure body

这三个 span 标签("声明在闭包体之外"、"仅在闭包体内有效"、"在此处逃逸出闭包体")不是随意拼凑的文案,而是源码中逐条构造出来的,下一节会给出对应的实现位置。

二、为什么参数类型标注会引入"新生命周期"

官方文档给出的解释只有两句,但这是理解 E0521 的核心规则:

A type annotation of a closure parameter implies a new lifetime declaration. Consider to drop it, the compiler is reliably able to infer them.

(闭包参数的类型标注意味着一个新生命周期声明;建议去掉标注,编译器能够可靠地推断出来。)

具体机制是:

  • 带标注时|el: &str| 等价于 |el: &'_ str|,其中 '_ 是一个绑定在闭包自身类型上的匿名生命周期(region 术语中叫 bound region)。闭包的函数类型要求该引用"恰好"活到这个闭包体结束,它不能与外部更长的生命周期统一,因此 el 无法被存进外部的 list
  • 不带标注时|el| 的参数类型由编译器根据期望类型(expected type)推断,此时参数引用拿到的是一个自由生命周期变量(free region,即类型推断变量)。它可以向上统一为更长的生命周期,list.push(el) 即可通过检查。

仓库中的 UI 测试 expect-region-supply-region.rs 完整验证了这两类 region 的行为差异,值得一读:

fn closure_expecting_bound<F>(_: F)
where
    F: FnOnce(&u32),          // 期望类型中的生命周期绑定在 F 上(bound)
{
}

fn closure_expecting_free<'a, F>(_: F)
where
    F: FnOnce(&'a u32),      // 期望类型中的生命周期是泛型参数(free)
{
}

fn expect_bound_supply_nothing() {
    let mut f: Option<&u32> = None;
    closure_expecting_bound(|x| {
        f = Some(x); //~ ERROR borrowed data escapes outside of closure
    });
}

fn expect_bound_supply_bound() {
    let mut f: Option<&u32> = None;
    closure_expecting_bound(|x: &u32| {   // 显式标注,同样报错
        f = Some(x); //~ ERROR borrowed data escapes outside of closure
    });
}

fn expect_free_supply_nothing() {
    let mut f: Option<&u32> = None;
    closure_expecting_free(|x| f = Some(x)); // OK
}

三个场景的结论(对应 expect-region-supply-region.stderr 的实际输出):

期望类型中的 region 闭包参数写法 结果
绑定生命周期(FnOnce(&u32) 无标注 |x| E0521
绑定生命周期(FnOnce(&u32) 显式标注 |x: &u32| E0521(与无标注相同)
自由生命周期(FnOnce(&'a u32) 无标注 |x| 编译通过

注意第二行:当期望类型已经把参数引用"钉死"为 bound region 时,写不写标注都会报错——此时问题不在你的标注,而在 API 设计;而当期望类型携带自由生命周期时(第三行),去掉标注即可让参数复用调用侧更长的生命周期。

三、E0521 在 rustc 中的实现位置

3.1 诊断构造函数

E0521 的诊断消息由 borrowed_data_escapes_closure 构造:

pub(crate) fn borrowed_data_escapes_closure<'diag>(
    dcx: DiagCtxtHandle<'diag>,
    escape_span: Span,
    escapes_from: &str,
) -> Diag<'diag> {
    struct_span_code_err!(
        dcx,
        escape_span,
        E0521,
        "borrowed data escapes outside of {}",
        escapes_from,
    )
}

消息末尾的 {escapes_from} 是由定义该 region 的 scope 的描述符(tcx.def_descr)填充的——所以错误标题既可能是 outside of closure,也可能是 outside of function(当逃逸作用体是函数时)。

3.2 两条触发路径

从源码结构看,E0521 有两条相互对应的触发路径,分别服务于 region 求解器与 MIR 阶段的冲突检查:

  1. Region 冲突路径region_errors.rs 中的 report_escaping_data_error。当 region 求解发现"被借用的 region 必须 outlive 一个更长 region"的约束不可满足时走到这里。它在基础消息之上叠加 span 标签:

    • `{outlived_fr}` declared here, outside of the {escapes_from} body —— 指向外部变量(如 listf)的声明处;
    • `{fr}` is a reference that is only valid in the {escapes_from} body —— 指向参数处;
    • `{fr}` escapes the {escapes_from} body here —— 指向逃逸赋值/调用处。

    若 region 无法命名(匿名临时借用),则退化为 a temporary borrow escapes the {escapes_from} body here,并给出 help:"{outlived_name} is declared outside the {escapes_from}, so any data borrowed inside the {escapes_from} cannot be stored into it"(声明在外面的变量,不能存放闭包内部借来的数据)。

  2. MIR 冲突路径conflict_errors.rs 中的 report_escaping_data。它在 MIR 数据流分析检测到借用冲突且成因是"upvar 借用逃逸"时输出同样的 E0521 基础消息,并附加 borrow is only valid in the {escapes_from} body 等标签。

两条路径产出的错误码与主消息一致,这解释了为什么同一份代码在不同 rustc 版本/不同检查阶段可能给出措辞略有差异但同为 E0521 的诊断。

四、与 E0373 的协作:一个真实诊断输出实例

测试 closure-bounds-static-cant-capture-borrowed.stderr 展示了 E0521 与 E0373 同时出现的典型场景——闭包捕获了函数参数 x: &(),而闭包又被要求 'static

error[E0521]: borrowed data escapes outside of function
  |
  | fn foo(x: &()) {
  |          -  - let's call the lifetime of this reference `'1`
  |          |
  |          `x` is a reference that is only valid in the function body
  |     bar(|| {
  |         let _ = x;
  |         ^
  |         `x` escapes the function body here
  |
  |         argument requires that `'1` must outlive `'static`

error[E0373]: closure may outlive the current function, but it borrows `x`, ...
help: to force the closure to take ownership of `x` (and any other referenced
      variables), use the `move` keyword

这里可以看到两类"逃逸"的区别:E0521 描述的是"引用参数/捕获的数据被存进了比它活得更长的地方",而 E0373 描述的是"闭包本身要求比它捕获的借用更长的存活期"。两者常常同报,但修复方向不同(move 只能解决 E0373 的所有权问题,解决不了 E0521 的 region 约束,见下文规避方案)。

五、规避 E0521 的实战方案

方案 1:去掉闭包参数的类型标注(文档推荐)

官方文档给出的直接修复就是删除标注,让编译器推断:

let mut list: Vec<&str> = Vec::new();

let _add = |el| {
    list.push(el);
};

此时 el 拿到自由生命周期,可与 list 的元素生命周期统一。官方文档同时建议参阅 The Rust Book 的 "Closure type inference and annotation" 一节与 Reference 的 "Lifetime elision" 章节,理解闭包参数推断与生命周期省略规则的细节。

方案 2:把外部存储改为按生命周期参数化

如果 f(或 list)的生命周期必须比闭包体长,就把"存"这一动作移出闭包,或让存储容器接受调用侧确定的生命周期:

fn collect<F>(f: F) -> Vec<u32>
where
    F: FnOnce(&u32) -> u32,
{
    vec![f(&42)]
}

即通过 FnOnce(&'a u32) 这类携带自由生命周期参数的 trait bound 传递引用,而非让闭包参数自行携带绑定生命周期。

方案 3:改存 owned 类型

Vec<&str> 换成 Vec<String>,闭包内做 list.push(el.to_owned()),从根本上消除"引用必须比作用体更长"的约束。

方案 4:让闭包接收外部可变引用,数据"流进"闭包

let mut list: Vec<&str> = Vec::new();
let add = |el: &str| list.push(el.to_owned()); // 或按方案 1 去标注
// 或者:闭包只负责消费引用,结果在闭包外收集

六、相关错误码速查

错误码 消息 与 E0521 的区别
E0521 borrowed data escapes outside of closure/function 引用参数/捕获数据被存入比其生命周期更长的外部位置
E0373 closure may outlive the current function, but it borrows x 闭包本身要求比其捕获借用更长的存活期,move 通常可修复
E0515 cannot return a reference to data owned by the current function 返回的是指向函数局部变量的引用
E0597 x does not live long enough 一般性的借用期不足,不涉及"逃逸出作用体"的结构

E0521 与 E0373 的构造位置都在 borrowck_errors.rs(E0373 见 cannot_capture_in_long_lived_closureE0515 见 cannot_return_reference_to_local),而 E0521 独有的两条"逃逸"诊断路径在 region_errors.rsconflict_errors.rs 中调用同一构造函数。

小结

  • E0521 的本质:一个绑定在闭包/函数自身上的引用生命周期,被要求 outlive 外部声明的变量(如 Vec<&str>Option<&u32>)。
  • 触发根源常是闭包参数的显式类型标注——它把参数的匿名生命周期变成闭包类型的绑定 region,从而切断与更长生命周期的统一;删除标注让推断接管,是最简单的修复。
  • 当期望类型(如 FnOnce(&u32))本身就携带绑定生命周期时,报错与标注无关,需要改 API 设计(引入 'a 参数)或改存 owned 类型。
  • 排查路径:rustc --explain E0521 对应文档即 E0521.md;若同屏出现 E0373,先判断逃逸的是"参数"还是"捕获",再决定用去标注还是 move
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389