Rust 编译器错误 E0070 详解:为什么赋值语句左侧必须是 place 表达式
本文围绕 rustc 错误码 E0070(invalid left-hand side of assignment,赋值语句的左侧不是有效的赋值目标)展开,基于 rust 编译器仓库中的错误码文档 E0070.md、真实 UI 测试用例 E0070.rs 及其期望输出 E0070.stderr,并结合类型检查器源码中 E0070 的实际触发点,完整讲解"place 表达式"的语义边界、四类典型报错场景、可编译的正确写法,以及诊断信息在 rustc_hir_typeck 中的生成链路。读完本文后,你可以准确判断任何赋值表达式左侧是否合法,并快速定位 E0070 的根因与修复方式。
一、E0070 的语义:对"非 place 表达式"执行了赋值
E0070 的错误标题是 An assignment operator was used on a non-place expression(赋值运算符被用于一个非 place 表达式)。
在 Rust 中,赋值语句 lhs = rhs 要求左侧(left-hand side, lhs)必须是一个 place 表达式。place 表达式代表一个可寻址的内存位置,根据错误码文档的定义,它可以是:
- 变量(可带命名空间限定),如
x、local.y; - 解引用(deref),如
*p; - 索引表达式,如
v[i]、arr[0]; - 字段引用(field reference),如
s.x。
只要左侧不满足上述任一形态——比如常量、字面量、函数调用、类型/结构体名——rustc 就会报告 E0070。文档同时指出,更多细节可参考 Reference 的 Expressions 章节("places, rvalues and temporaries" 部分):Rust 将表达式严格区分为 place(可寻址)、rvalue(临时值)与 temporary,E0070 正是这一区分的直接体现。
1.1 典型的错误写法
错误码文档中给出的完整错误示例(标注了 compile_fail,E0070)如下,四种赋值目标全部非法:
// 对应仓库测试 tests/ui/error-codes/E0070.rs 的扩展版本
struct SomeStruct {
x: i32,
y: i32,
}
const SOME_CONST: i32 = 12;
fn some_other_func() {}
fn some_function() {
SOME_CONST = 14; // error: a constant value cannot be changed!
1 = 3; // error: 1 isn't a valid place!
some_other_func() = 4; // error: we cannot assign value to a function!
SomeStruct::x = 12; // error: SomeStruct is a structure name but
// it is used like a variable!
}
四类报错场景分别对应不同的"非 place"来源:
| 左侧写法 | 为什么不是 place | 直观解释 |
|---|---|---|
SOME_CONST |
常量绑定不是可变内存位置 | 常量值不可改变 |
1(字面量) |
字面量是 rvalue,无内存地址 | 1 根本不是存储位置 |
some_other_func() |
函数调用结果是临时值(rvalue) | 不能给一个"值"赋值 |
SomeStruct::x |
结构体类型名不是变量 | 类型/结构名被当成了变量 |
1.2 编译器实际输出的诊断信息
仓库中的 UI 测试 E0070.rs 保留了前三类场景,其期望诊断输出 E0070.stderr 展示了 rustc 真实的报错格式:
error[E0070]: invalid left-hand side of assignment
--> $DIR/E0070.rs:6:16
|
LL | SOME_CONST = 14;
| ---------- ^
| |
| cannot assign to this expression
error[E0070]: invalid left-hand side of assignment
--> $DIR/E0070.rs:7:7
|
LL | 1 = 3;
| - ^
| |
| cannot assign to this expression
error[E0070]: invalid left-hand side of assignment
--> $DIR/E0070.rs:8:23
|
LL | some_other_func() = 4;
| ----------------- ^
| |
| cannot assign to this expression
可以看到诊断的固定结构:主标题为 invalid left-hand side of assignment,子标签 cannot assign to this expression 指向 = 右侧位置;SomeStruct::x 这一变体没有包含在该测试文件中,属于文档中额外补充的第四类场景。这也说明错误码文档与 UI 测试用例是相互印证的两份证据来源。
二、合法写法:四类 place 表达式逐一对照
错误码文档给出的"可运行示例"覆盖了三类合法 place——字段赋值与解引用赋值(变量本身是最基础的一类):
struct SomeStruct {
x: i32,
y: i32,
}
// 原文档示例整理为可编译形式(增加 main 与函数包装)
fn main() {
let mut s = SomeStruct { x: 0, y: 0 };
s.x = 3; // that's good ! —— 字段引用是 place
// ...
}
fn some_func(x: &mut i32) {
*x = 12; // that's good ! —— 解引用是 place
}
说明:原文档代码片段中 let mut s = ... 直接位于顶层,这里仅为其加上 fn main 包装使其真正可编译,赋值语句本身与原文档完全一致。
结合测试文件与参考语义,四类 place 完整对照如下:
// 1. 变量(可选命名空间)
fn f() {
let mut x: i32 = 0;
x = 1; // OK:局部变量
}
// 2. 解引用
fn g(p: &mut i32) {
*p = 2; // OK:通过引用改写目标内存
}
// 3. 索引表达式
fn h(v: &mut Vec<i32>) {
v[0] = 3; // OK:索引表达式是 place
}
// 4. 字段引用
fn i(s: &mut SomeStruct) {
s.y = 4; // OK:结构体字段
}
一个容易踩的坑:mut 是前提。place 表达式的"可赋值性"还要求底层绑定可变(let mut、&mut 等)。如果左侧是 place 但底层不可变,rustc 报告的将不是 E0070,而是 E0384(cannot assign to immutable variable)——这属于另一条诊断路径。
三、源码级溯源:E0070 在 rustc 中如何被触发
从源码结构看,E0070 的诊断生成集中在 HIR 类型检查器 rustc_hir_typeck 中:
3.1 赋值检查入口:check_lhs_assignable
在 expr.rs 中定义了 check_lhs_assignable 方法,它是所有"赋值类"语句共用的左侧合法性校验入口。在普通赋值表达式(lhs = rhs)的检查流程中,完成右值类型检查与强制转换后,编译器显式调用该方法并传入错误码 E0070:
// compiler/rustc_hir_typeck/src/expr.rs (约 L1305)
self.check_lhs_assignable(lhs, E0070, span, |err| {
if let Some(rhs_ty) = self.typeck_results.borrow().expr_ty_opt(rhs) {
suggest_deref_binop(err, rhs_ty);
}
});
值得注意的是这个调用附带的回调:当左侧不可赋值时,如果右侧类型允许,诊断会尝试给出"解引用二元运算"的修复建议(suggest_deref_binop),即在报错的同时机器可应用地建议用户补一个 *。
check_lhs_assignable 内部构造的正是测试文件中看到的那条诊断,主标题在 expr.rs 附近生成:
let mut err = self.dcx().struct_span_err(op_span, "invalid left-hand side of assignment");
随后补充 cannot assign to this expression 标签,与 E0070.stderr 中的输出逐字对应。
3.2 同一错误码在其他赋值形态中复用
E0070 并不只在 = 表达式中出现。搜索 E0070 可以看到它在多个赋值相关检查中被复用:
- fn_ctxt/checks.rs:在处理语句级赋值/复合赋值等场景时,源码注释说明"如果左侧不是句法层面的 place 表达式,E0070 会先行触发,此处冗余错误会被静默抑制"——这解释了为什么一条非法语句通常只报一次 E0070 而不是级联出多条错误;
- demand.rs:类型需求(type demand)路径中的注释同样声明"已发出 E0070 'invalid left-hand side of assignment'",用于避免对同一位置重复诊断。
可以推断,编译器刻意用"首个 E0070 胜出、后续冗余诊断静默"的策略保证报错的可读性——这与测试输出中每条非法赋值恰好对应一条 error 的行为一致。
四、排查与修复清单
遇到 error[E0070]: invalid left-hand side of assignment 时,按以下顺序排查:
- 确认左侧是否落入四类 place:变量、解引用、索引、字段引用。字面量(
1 = 3)、常量(SOME_CONST = 14)、函数调用(f() = 4)、类型/结构名(SomeStruct::x = 12)都非法,直接改写为合法的存储位置; - 区分"不可变"与"非 place":如果左侧是变量但缺少
mut,报的是 E0384 而非 E0070;E0070 意味着左侧根本没有对应的内存位置; - 函数调用返回引用时:若本意是
f() = 4而f()返回&mut T,可先绑定到变量再赋值:let r = f(); *r = 4;——这符合编译器suggest_deref_binop的建议方向; - 验证方式:仓库中 tests/ui/error-codes/E0070.rs 展示了 rustc 对每类非法左侧的标准诊断;修改代码后可直接
rustc --explain E0070查看与本节一致的解释文档。
五、小结
E0070 是 Rust 类型系统对"赋值必须指向可寻址位置"这一原则的守卫:左侧必须是变量、解引用、索引或字段引用这四类 place 表达式之一,常量、字面量、函数调用与类型名都不合格。错误码文档 E0070.md 给出了全部典型反例与正例,UI 测试 E0070.stderr 固化了真实诊断文本,而 rustc_hir_typeck 中 check_lhs_assignable 的调用点 及其冗余诊断抑制逻辑则说明了该错误码"只报一次、附带修复建议"的行为来源。理解了 place 与 rvalue 的边界,E0070 的每一种触发场景与修复路径就都可以直接推导出来。
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