Rust 错误 E0506 深度解析:向被借用变量赋值(cannot assign to ... because it is borrowed)
导读
E0506 是 Rust 编译器在借用检查(borrow checking)阶段报告的一类错误:当一个值(或其字段)仍处于被借用(borrowed)状态时,代码又试图对它进行整体赋值。本文以 E0506.md 为核心骨架,结合 rustc 借用检查器的真实源码(rustc_borrowck)与其 UI 测试用例,深入讲解 E0506 的触发场景、错误背后“别名 + 可变性”的根因,并给出三种可落地的修复方案。读完本文,你将能在真实项目中快速识别、复现并修复这类借用冲突,同时理解该错误码在编译器内部的生成链路。
E0506 是什么
E0506 的官方描述只有一句话:
An attempt was made to assign to a borrowed value.
其完整错误文本形如:
error[E0506]: cannot assign to `fancy_num` because it is borrowed
它属于 Rust 编译器的借用检查错误族。当你在 fancy_num 上持有共享引用(&fancy_num)期间,又试图对整个 fancy_num 执行赋值(assign,= 整体覆盖),编译器就会报出 E0506。注意它与“移动(move)”类错误(E0505/E0507)和“冲突借用(conflicting borrow)”类错误(E0502)在语义上的差别,这一点在后文会专门辨析。
官方最小复现示例
以下代码来自 E0506.md 的“Erroneous code example”,可直接复制并编译复现:
struct FancyNum {
num: u8,
}
let mut fancy_num = FancyNum { num: 5 };
let fancy_ref = &fancy_num;
fancy_num = FancyNum { num: 6 };
// error: cannot assign to `fancy_num` because it is borrowed
println!("Num: {}, Ref: {}", fancy_num.num, fancy_ref.num);
为什么编译器拒绝这段代码
文档给出的解释非常关键:因为 fancy_ref 仍然持有指向 fancy_num 的引用,所以不能把 fancy_num 赋成一个新值,否则会使该引用失效。
从内存模型看:&fancy_num 一旦创建,借用检查器就要求在借用存续期间,被借用路径上不允许出现可改变该路径的写操作。fancy_num = FancyNum { num: 6 } 会把整个结构体原地覆盖,属于对整条路径的重写;若放行,随后 fancy_ref.num 读取到的既可能是旧值也可能是新值,破坏共享引用“只读别名”的不变量。这正是 Rust 在编译期就消除数据竞争(data race)和悬垂引用(dangling reference)的核心手段。
值得强调的是:E0506 针对的是对整条被借用路径做赋值(整变量覆盖 x = ...),而不是单纯地修改被借用对象内部、不与引用重叠的字段(后者若与借用路径重叠,会被归入 E0506 之外的冲突借用逻辑)。关于“赋值”与“移动”如何分派到不同的错误码,本文末尾会给出源码依据。
修复方案一:先移出再赋值(move 语义)
如果旧的 fancy_num 不再需要,可以先把它的值**移出(move)**到一个新绑定 moved_num,此时借用关系解除,随后便能安全地对 fancy_num 赋新值:
struct FancyNum {
num: u8,
}
let mut fancy_num = FancyNum { num: 5 };
let moved_num = fancy_num; // 所有权从 fancy_num 转移到 moved_num
fancy_num = FancyNum { num: 6 }; // 现在 fancy_num 已无借用,赋值合法
println!("Num: {}, Moved num: {}", fancy_num.num, moved_num.num);
适用前提:原值必须可以被整体移走(未被借用、类型本身满足所有权转移要求),并且代码语义上允许“旧值换主、新值上位”。FancyNum 不含 Drop、不含共享所有权字段时,移出完全合法。
修复方案二:用作用域块收窄借用生命周期
如果代码确实需要借用该值(例如先读取、后重写),官方建议把借用限制在一个作用域块内,让借用随块结束而终止:
struct FancyNum {
num: u8,
}
let mut fancy_num = FancyNum { num: 5 };
{
let fancy_ref = &fancy_num;
println!("Ref: {}", fancy_ref.num);
} // fancy_ref 在此离开作用域,借用结束
// 编译通过:fancy_ref 已不在作用域内
fancy_num = FancyNum { num: 6 };
println!("Num: {}", fancy_num.num);
这里的本质是让借用区域(borrow region)在词法上更短,从而不再覆盖后续的赋值语句。借用检查器据此认定赋值发生时该路径上已不存在活跃借用。
修复方案三:把引用传入函数,借用随调用结束
另一种收窄借用生命周期的常用手段,是把引用作为参数传入函数。函数返回后,由本次调用产生的借用即告终止,调用点之后即可放心赋值:
struct FancyNum {
num: u8,
}
fn print_fancy_ref(fancy_ref: &FancyNum) {
println!("Ref: {}", fancy_ref.num);
}
let mut fancy_num = FancyNum { num: 5 };
print_fancy_ref(&fancy_num); // 借用仅存在于本次函数调用期间
// 编译通过:函数借用在调用结束后已终止
fancy_num = FancyNum { num: 6 };
println!("Num: {}", fancy_num.num);
该模式在真实代码中随处可见:把“只读访问逻辑”抽成接收 &T 的函数,既能让借用边界清晰,也能自然规避 E0506。
源码视角:E0506 在编译器内部如何生成
仅知道“怎么改”还不够,理解错误在 rustc 内部的产生链路能帮你举一反三。
错误码的构造点
E0506 的诊断由 borrowck_errors.rs 中的 cannot_assign_to_borrowed 构造:
pub(crate) fn cannot_assign_to_borrowed(
&self,
span: Span,
borrow_span: Span,
desc: &str,
) -> Diag<'diag> {
struct_span_code_err!(
self.dcx(),
span,
E0506,
"cannot assign to {} because it is borrowed",
desc,
)
.with_span_label(borrow_span, format!("{desc} is borrowed here"))
.with_span_label(span, format!("{desc} is assigned to here but it was already borrowed"))
}
从中可以看到实际报错的两个核心标注:借用在何处发生(... is borrowed here)以及赋值在哪里与活跃借用重叠(... is assigned to here but it was already borrowed)。阅读 E0506 完整解释文档即可看到与之一致的措辞。此外,cannot_assign_to_borrowed 同文件中还“家族式”定义了邻近错误码的构造函数:cannot_move_out_of 对应 E0507、cannot_reassign_immutable 对应 E0384、cannot_reborrow_already_borrowed 对应 E0502——这说明 E0506 与它们共享同一套诊断基础设施,仅因冲突“动作类型”不同而被路由到不同错误码。
调用链:从“非法写操作”到 E0506
- 借用检查在 lib.rs 中依据
WriteKind分流冲突:WriteKind::Mutate与WriteKind::Replace(即对路径的修改和替换赋值)都会进入report_illegal_mutation_of_borrowed;WriteKind::Move则走report_move_out_while_borrowed(对应 E0505/E0507 等移动类错误)。也就是说,“赋值”与“移动”在此处被区分开,赋值走向 E0506 这条链路。 - conflict_errors.rs 的
report_illegal_mutation_of_borrowed会最终调用上文cannot_assign_to_borrowed生成 E0506 诊断,并在闭包/协程捕获场景下补充“变量在闭包中被借用”的成因标注,再通过buffer_error统一缓冲输出。
官方 UI 测试佐证
tests/ui/borrowck/borrowck-assign-comp.rs 用三种角度验证 E0506,几乎覆盖了该错误的全部典型形态(其 .stderr 中逐条记录了 error[E0506]):
struct Point { x: isize, y: isize }
fn a() {
let mut p = Point {x: 3, y: 4};
let q = &p; // 借用整个 p
p.x = 5; // ~ ERROR cannot assign to `p.x` because it is borrowed
q.x;
}
fn c() {
let mut p = Point {x: 3, y: 4};
let q = &p.y; // 仅借用 p.y
p = Point {x: 5, y: 7}; // ~ ERROR cannot assign to `p` because it is borrowed
*q;
}
fn d() {
let mut p = Point {x: 3, y: 4};
let q = &p.y;
p.y = 5; // ~ ERROR cannot assign to `p.y` because it is borrowed
*q;
}
注意三者的微妙差别:
fn a:借的是整个p,赋值的是其字段p.x——只要引用覆盖了写路径(此处字段包含在被借用的整条路径内),即属非法;fn c:只借了内部字段p.y,却试图整体覆盖p,同样非法(整体重写会殃及p.y);fn d:借p.y又写p.y,直接命中同一路径。
这说明 E0506 关心的是“写路径是否与活跃借用的路径重叠”,而不是简单的“同一个变量”。
另一个来自真实缺陷的回归测试是 cannot-assign-borrowed-ref-in-slice.rs(对应 issue #40288):把 &i32 存入切片会延长对 refr 的借用,随后 *refr = 3 即报 E0506:
fn save_ref<'a>(refr: &'a i32, to: &mut [&'a i32]) {
for val in &mut *to {
*val = refr; // refr 的内容被存入切片,借用随之延长
}
}
fn main() {
let ref init = 0i32;
let ref mut refr = 1i32;
let mut out = [init];
save_ref(&*refr, &mut out);
*refr = 3; //~ ERROR cannot assign to `*refr` because it is borrowed
println!("{:?}", out[0]);
}
这个例子解释了借用“外流”的一种隐蔽场景:即使代码中没有显式书写长的借用,借用在被存入其他容器后其生命周期也会被撑长,程序员必须意识到这一点。
与相邻错误码的辨析
在日常修复中,开发者常把 E0506 与 E0502、E0505/E0507 混淆,这里给出快速区分表:
| 错误码 | 典型触发形态 | 一句话判定 |
|---|---|---|
| E0506 | 值被借用时执行赋值 x = ... |
“写 = 整体重写”撞上活跃借用 |
| E0502 | 值已被借用时再去创建新的借用(如同时取 & 与 &mut) |
借了还借,且两者冲突 |
| E0505/E0507 | 值被借用时试图移出/转移所有权 | 借用期间被“掏空” |
| E0384 | 向不可变绑定重复赋值 | 与借用无关,只是 mut 缺失 |
判定口诀:E0506 的三个修复方向(移出旧值、作用域收窄、函数收窄借用的边界)从本质上都在回答同一个问题——让赋值发生时,该路径上不再存在活跃的共享借用。
实践自查清单
遇到形如 error[E0506]: cannot assign to ... because it is borrowed 的错误时,按以下顺序排查:
- 找到报错标注中的两个位置:“borrowed here”和“assigned to here”;
- 判断赋值是否真的发生在借用存活区间内(必要时沿代码向上看引用是否被传入函数或存入容器而延长);
- 若旧值不再需要 → 优先“移出再赋值”(方案一);
- 若旧值还需要只读访问 → 用作用域块(方案二)或把读取逻辑封装进函数(方案三)压缩借用区间;
- 确认赋值对象不是被借用的路径本身,若你本意只是改一个字段,请检查字段是否与引用路径重叠。
延伸阅读
- 官方错误码文档 E0506:本文的原始素材,包含全部四段可编译示例;
- borrowck_errors.rs 错误码构造区:E0506/E0507/E0502/E0384 等诊断的统一出处;
- conflict_errors.rs 的 report_illegal_mutation_of_borrowed:E0506 的最终组装与缓冲输出逻辑;
- lib.rs 的 WriteKind 分流逻辑:理解“赋值(Mutate/Replace) → E0506,移动(Move) → 移动类错误”的分派依据;
- borrowck-assign-comp.rs:E0506 三种典型形态的 UI 测试;
- cannot-assign-borrowed-ref-in-slice.rs:借用被容器延长后触发的回归测试。
掌握 E0506 不仅在于记住修复句式,更在于建立“借用区域 × 写路径重叠”的心智模型——这正是 rustc 借用检查器在 lib.rs 中所做的核心判断。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00