☰
Rust 错误 E0506 深度解析:向被借用变量赋值(cannot assign to ... because it is borrowed)
2026/11/5 1:52:30 网站建设 项目流程

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; }

注意三者的微妙差别:

  1. fn a:借的是整个p,赋值的是其字段p.x——只要引用覆盖了写路径(此处字段包含在被借用的整条路径内),即属非法;
  2. fn c:只借了内部字段p.y,却试图整体覆盖p,同样非法(整体重写会殃及p.y);
  3. 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的错误时,按以下顺序排查:

  1. 找到报错标注中的两个位置:“borrowed here”和“assigned to here”;
  2. 判断赋值是否真的发生在借用存活区间内(必要时沿代码向上看引用是否被传入函数或存入容器而延长);
  3. 若旧值不再需要 → 优先“移出再赋值”(方案一);
  4. 若旧值还需要只读访问 → 用作用域块(方案二)或把读取逻辑封装进函数(方案三)压缩借用区间;
  5. 确认赋值对象不是被借用的路径本身,若你本意只是改一个字段,请检查字段是否与引用路径重叠。

延伸阅读

  • 官方错误码文档 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询