Rust 错误 E0506 深度解析:向被借用变量赋值(cannot assign to ... because it is borrowed)
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
导读
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 中所做的核心判断。
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考