首页
/ Rust 编译错误 E0503 深度解析:可变借用期间使用原值的原因、修复与编译器实现

Rust 编译错误 E0503 深度解析:可变借用期间使用原值的原因、修复与编译器实现

2026-09-07 09:59:43作者:翟萌耘Ralph

E0503 是 rustc 借用检查器在“一个值仍处于可变借用(mutable borrow)状态时,又通过原值对该值进行了读取/使用”这一场景下抛出的经典编译错误,报错文案为 cannot use ... because it was mutably borrowed。本文以 compiler/rustc_error_codes/src/error_codes/E0503.md 为核心依据,结合 rustc 借用检查源码与回归测试,讲清它的触发条件、Rust 借用规则背后的设计动机、两种标准修复路径,以及它在编译器中从检出到报错的全过程。

E0503 是什么:错误速览

在 Rust 的借用规则中,同时只能存在一个对某值的可变借用,并且可变借用存续期间,原值不允许被再次读取或使用。E0503 针对的正是最后这条约束:当你已经通过 &mut 借用了某个值,却仍然直接通过原变量去使用(例如做算术运算、读取字段、传入函数等)该值时,借用检查器会拒绝编译。

需要特别注意的是,E0503 的场景里并不存在第二个借用,只有一个活着的可变借用加一个“对原值的直接使用”。这使它区别于同族的其他借用错误:若是尝试在已有可变借用时再创建第二个可变借用,得到的通常是 E0499(cannot borrow ... as mutable more than once at a time)一类冲突报错;而 E0503 关注的是“借用未结束时对原值本体(而非借用手柄)的越权使用”。

可复现的最小失败示例

原文档给出了如下最小示例(该代码在编译测试中以 compile_fail,E0503 标注,即预期编译失败且错误码为 E0503):

fn main() {
    let mut value = 3;
    // Create a mutable borrow of `value`.
    let borrow = &mut value;
    let _sum = value + 1; // error: cannot use `value` because
                          //        it was mutably borrowed
    println!("{}", borrow);
}

逐行拆解这段代码的执行意图:

  1. let mut value = 3; 声明一个可变的整数变量;
  2. let borrow = &mut value;value 创建独占的可变借用borrow 成为访问该内存的唯一合法途径;
  3. let _sum = value + 1; 越过 borrow 直接读取原值 value——这正是被禁止的行为,触发 E0503;
  4. println!("{}", borrow); 尝试打印借用,但因上一步已报错,程序无法通过编译。

编译器实际产出的诊断信息(由 borrowck_errors.rs 中的诊断构造逻辑生成)大致包含主错误消息以及两个关键 span 标注:

  • let borrow = &mut value; 处标注 “value is borrowed here”;
  • value + 1 处标注 “use of borrowed value”。

这两条标注帮助开发者快速定位:借用发生在哪一行、越权使用又发生在哪一行。

为什么这是被禁止的:可变借用的本质

原文档指出,这段代码无法编译是因为它“violate Rust's mutability rules(违反 Rust 的可变性规则)”。要真正理解 E0503,需要回到 Rust 的**别名与可变性(aliasing and mutation)**这一核心设计:

  • 可变借用(&mut T)是独占的访问通道,编译器借此保证在借用存续期间,不存在任何其他方式可以读取或修改同一块数据;
  • 如果允许在持有 &mut 的同时直接使用原值,那么同一块内存就会同时存在“可变借用手柄”和“原值路径”两条访问入口,二者构成别名(alias)。通过原值做 value + 1 时读到的是旧值还是新值将无法确定,可变借用“独占”的前提也就被打破了;
  • 这种独占性让编译器可以放心做大量优化(缓存、重排、并行化等),因为只要一个 &mut 活着,就不可能有“看不见的”读写悄悄发生。E0503 正是这一保证在编译期的强制校验点。

换句话说,&mut value 不只是“把访问权暂时交出”,而是“在借用存续期内,原变量 value 对这块内存的访问权被整体冻结”。

修复方案一:先结束借用,再使用原值

原文档给出的第一种也是最自然的修复思路是——调整代码顺序,让可变借用先“寿终正寝”,再触碰原值。借用终止后,原值恢复可访问状态:

fn main() {
    let mut value = 3;
    let borrow = &mut value;
    println!("{}", borrow);
    // The block has ended and with it the borrow.
    // You can now use `value` again.
    let _sum = value + 1;
}

这里的关键在于:println!("{}", borrow)borrow最后一次使用。依据 Rust 借用检查的非词法生命周期(NLL,non-lexical lifetimes)机制,借用并不必然持续到其声明所在代码块的结尾,而是持续到borrow 的最后一次使用为止

  • 在这个版本中,最后一次使用 borrowprintln! 那一行;
  • println! 结束到 let _sum = value + 1; 之间,borrow 已不再被使用,借用随之结束;
  • 因此后续的 value + 1 是合法的——两个操作在时间上被完全隔开,互不重叠。

原文档注释“The block has ended and with it the borrow”描述的是借用作用域的直观概念;在 NLL 下更精确的说法是:当最后一次使用 borrow 的语句执行完毕后,借用区域即告终止,无需等到整个 fn main 结束。

这种“用完即放”的顺序调整是处理 E0503 的首选方案:它不引入任何额外开销,也完全保留了后续对 value 的直接读写能力。前提是你确实需要先把借用的结果消费完、再回到原值上操作。

修复方案二:在借用前克隆一份独立副本

如果业务上确实存在“一边持有可变借用,一边还要读取原值内容”的需求,那么将原值克隆一份独立副本是原文档给出的第二种修复路径:

fn main() {
    let mut value = 3;
    // We clone `value`, creating a copy.
    let value_cloned = value.clone();
    // The mutable borrow is a reference to `value` and
    // not to `value_cloned`...
    let borrow = &mut value;
    // ... which means we can still use `value_cloned`,
    let _sum = value_cloned + 1;
    // even though the borrow only ends here.
    println!("{}", borrow);
}

这段代码能通过编译的机理在于别名关系的改变:

  • value.clone() 在堆/栈上产出一块全新的数据 value_cloned,它与 value 不再共享内存;
  • &mut value 借用的目标地址与 value_cloned 完全不同,二者不再构成别名;
  • 于是在 borrow 仍然存活的期间(示例中一直持续到最后的 println!("{}", borrow);),使用 value_cloned 并不会触及被借用的那块内存,自然不违反独占规则。

值得注意的是,克隆方案有成本考量:对 i32 这类实现了 Copy 的简单类型,克隆近乎无开销;但对堆上的大型 StringVec 等,.clone() 会带来真实的内存分配与数据拷贝。因此工程上应优先尝试方案一(缩小/调整借用区间),仅在确实需要“借用与读原值并存”时才引入克隆,或进一步考虑改写为 Cell/RefCell 等内部可变性工具(后者属于另一个话题,本文不展开)。

从源码看 E0503 在编译器中如何被触发与报告

作为 rustc 的错误码文档体系成员,E0503 由一条贯穿多个 crate 的链路支撑,我们可以从当前仓库的实际代码确认它的完整生命周期:

1. 错误码注册。 所有错误码统一在 compiler/rustc_error_codes/src/lib.rserror_codes! 宏清单中登记(清单中 0503 位于 0500–0510 这一段借用检查错误码区间)。该库的注释同时说明:错误码解释统一放在 error_codes/EXXXX.md 文件中,且内容格式遵循 RFC 1567 的规范化要求;tidy 工具会通过 check_error_codes_docs 校验文档与宏清单的一致性。也就是说,本篇文章对应的 E0503.md 并非孤立注释,而是被编译器文档系统正式引用的注册资源。

2. 诊断实体构造。 E0503 的 Diag 由借用检查 crate 中的 MirBorrowckCtxt::cannot_use_when_mutably_borrowed 方法生成,见 compiler/rustc_borrowck/src/borrowck_errors.rs。源码中它通过 struct_span_code_err! 宏指定错误码 E0503 与主消息模板 "cannot use {desc} because it was mutably borrowed",随后追加两条 span 标注:

  • 对借用发生处:"{borrow_desc} is borrowed here"
  • 对越权使用处:"use of borrowed {borrow_desc}"

从源码结构可以推断:函数签名中的 spanborrow_span 分别对应“违规使用点”与“原始借用点”,desc/borrow_desc 是对两个位置涉及的 place(内存位置)的可读描述,通常就是变量名 value 这类内容——这正是你在终端看到错误提示“human-readable”的原因。

3. 触发判定与调用点。 是否确实发生“在可变借用存活期使用原值”由借用检查的数据流分析决定。位于 compiler/rustc_borrowck/src/diagnostics/conflict_errors.rsreport_use_while_mutably_borrowed 负责在确认冲突后组织一次完整报告:它取出借用 span 与使用 span,调用上文所述构造方法生成主诊断,并进一步处理闭包捕获、借用原因解释等补充信息。该函数在 compiler/rustc_borrowck/src/lib.rs 处被借用检查主流程调用,与同一诊断文件中的其他报告函数(如 cannot_mutably_borrow_multiply 对应 E0499、cannot_uniquely_borrow_by_one_closure 对应 E0500)共同构成 MIR 借用检查器面向用户的错误面。

4. 回归测试守护。 仓库中的 UI 测试夹具 tests/ui/error-codes/E0503.rs 与其期望输出 tests/ui/error-codes/E0503.stderr 将本错误码的触发场景固化下来:测试以 compile_fail 形式断言示例必须编译失败且错误码为 E0503,同时用 .stderr 精确比对诊断文本与 span 标注。这意味着 E0503.md 顶部代码块开头的 compile_fail,E0503 元数据注释并非摆设,而是 compiletest 测试框架识别“此段代码应产生该错误码”的指令。

E0503 与其兄弟错误码的区分

理解 E0503 时最容易混淆的是它与“可变借用重复借用”类错误的边界。借助 borrowck_errors.rs 这一文件即可从实现层面做出可靠区分:

错误码 诊断函数 触发情境 主消息示意
E0503 cannot_use_when_mutably_borrowed 已有一个可变借用存活,直接使用原值 cannot use ... because it was mutably borrowed
E0499 cannot_mutably_borrow_multiply 已有一个可变借用存活,再创建第二个可变借用 cannot borrow ... as mutable more than once at a time
E0500 cannot_uniquely_borrow_by_one_closure 闭包捕获导致的独占借用冲突 闭包要求独占访问某值的场景

简记法则:E0503 的“违规动作”是读/用原值(连只读的 value + 1 都不行,因为只要存在可写别名,读本身就会破坏 &mut 的排他性);E0499 的违规动作是再造一个 &mut;而 E0502(不可变借用与可变借用共存)等更多场景属于借用家族的其余成员,各自的修复思路本质上都是“缩短某个借用的生命周期,或消除别名”。

速查:遇到 E0503 时的自检清单

把本文内容压缩成排查路径,当你在 cargo build / cargo check 中看到 E0503 时,可按以下顺序自查:

  1. 找到两处标注:哪里“borrowed here”(可变借用产生点),哪里“use of borrowed …”(违规使用点);
  2. 判断两者是否有重叠的必要:若违规使用点只是碰巧写在借用之后、实际并不依赖借用结果,直接调整语句顺序(方案一),让借用在使用点之前自然结束;
  3. 若确实需要在借用存活期间读取同值内容:评估克隆副本(方案二)的成本;对大型数据可考虑改用共享不可变借用配合内部可变性等手段重构数据结构;
  4. 确认没有其他借用冲突:若错误提示中同时出现“more than once”措辞,说明你面对的更可能是 E0499 而非 E0503,二者修复方向不同。

以上四种自查方向全部有对应源码或测试佐证,详见 E0503.md(原理解释与双修复示例)、borrowck_errors.rs(诊断构造)、lib.rs(借用检查主流程调用点)与 tests/ui/error-codes/E0503.rs(行为契约测试)。想进一步系统性掌握可变借用与所有权机制,可阅读《The Rust Programming Language》一书的 References & Borrowing 章节,它与本文描述的 E0503 属于同一套语言规则体系。

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

项目优选

收起
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.16 K
2.78 K
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
904
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
932
1.86 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
862
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.95 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.38 K
1.47 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
535
606
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
549
398
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Markdown
77
23