首页
/ Rust 错误 E0506 深度解析:向被借用变量赋值(cannot assign to ... because it is borrowed)

Rust 错误 E0506 深度解析:向被借用变量赋值(cannot assign to ... because it is borrowed)

2026-09-07 10:15:45作者:尤峻淳Whitney

导读

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::MutateWriteKind::Replace(即对路径的修改和替换赋值)都会进入 report_illegal_mutation_of_borrowedWriteKind::Move 则走 report_move_out_while_borrowed(对应 E0505/E0507 等移动类错误)。也就是说,“赋值”与“移动”在此处被区分开,赋值走向 E0506 这条链路
  • conflict_errors.rsreport_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 不仅在于记住修复句式,更在于建立“借用区域 × 写路径重叠”的心智模型——这正是 rustc 借用检查器在 lib.rs 中所做的核心判断。

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

项目优选

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