首页
/ Rust 编译器错误 E0067 全解析:复合赋值运算符左侧为什么"不可赋值"

Rust 编译器错误 E0067 全解析:复合赋值运算符左侧为什么"不可赋值"

2026-09-07 12:55:03作者:柏廷章Berta

导读

E0067 是 Rust 编译器在类型检查阶段报出的错误,含义是"在赋值操作(如 =+=-= 等)的左侧使用了非法表达式"。本文以本仓库中 错误码说明文档 为核心骨架,结合 rustc 的类型检查源码,深入讲解什么是"可赋值的左侧"(place expression)、E0067 与相邻错误码(如 E0070E0368)的区别,以及遇到该错误时如何定位并修复代码。读完本文,你将能准确判断一条复合赋值语句的左侧是否合法,并掌握从源码层面排查此类错误的能力。

一、错误速览:一条会触发 E0067 的代码

原文档给出了最直接的触发示例——对整数字面量使用复合赋值运算符:

12 += 1; // error!

这一行代码会同时暴露两个问题:

  1. 字面量 12 是一个值表达式(value expression),不是可赋值的存储位置,因此"把 12 自增 1 再写回 12"在语义上根本无从谈起;
  2. rustc 在类型检查阶段检测到左侧不可赋值,于是发出 E0067

同样的思路也适用于普通赋值 12 = 1;,不过那会触发它的"邻居"错误 E0070(invalid left-hand side of assignment)。E0067 专门用于 a <op>= b 这种二元复合赋值运算符,而普通 = 赋值则由 E0070 负责,这一点下文会从源码路径详细展开。

二、正确写法:先声明可变变量,再复合赋值

原文档给出的修复方案是:为左侧提供一个真正"可写的存储位置"——一个 mut 变量:

let mut x: i8 = 12;
x += 1; // ok!

修复要点有三条,缺一不可:

  • 使用 let 声明变量:变量路径是对内存中一个槽位的引用,属于可赋值位置;
  • 声明为 mut:Rust 的变量默认不可变,对非 mut 变量做 x += 1 会触发 E0384(cannot assign twice to immutable variable),因此必须显式允许写入;
  • 类型标注 i8:让 x += 1 两侧类型清晰一致,避免溢出等后续问题。

执行完毕后 x 的值为 13,复合赋值等价于先读取、计算再写回。

三、核心概念:place expression 与 value expression

要彻底理解 E0067,必须先掌握 Rust 对表达式的一对基本分类。原文档中出现的 "place expression" 一词是理解本错误的钥匙,它对应的中文术语是位置表达式(place expression,过去也称左值 lvalue)。

  • 位置表达式(place expression):表示内存中某个可寻址的存储位置,对它求值得到的是"这个位置在哪里",而不是"这个位置里存的值是多少"。因为代表一个真实的存储槽位,它才可能成为赋值的左侧;
  • 值表达式(value expression):只产生一个值本身,不绑定任何可写存储位置。字面量、函数调用结果、二元运算结果、Box::new(..)LinkedList::new() 等都属于这一类。

赋值语句的语义是"把右侧的值写入左侧所代表的位置",因此左侧必须是 place expression。E0067 的英文全称 "An invalid left-hand side expression was used on an assignment operation" 说的正是:复合赋值运算符左侧被放了一个 value expression。

四、rustc 源码视角:编译器如何判定"左侧可赋值"

4.1 判定入口:is_syntactic_place_expr

rustc 判断一个表达式是否为位置表达式,核心实现在 compiler/rustc_hir/src/hir.rsis_syntactic_place_expr 是对通用判定函数 is_place_expr 的包装:

pub fn is_syntactic_place_expr(&self) -> bool {
    self.is_place_expr(|_| true)
}

从源码可以看到,下述表达式会被认定是位置表达式(可作赋值左侧):

  • 解析路径 ExprKind::Path(QPath::Resolved(..)),且目标解析为局部变量(Res::Local)、静态变量(Res::Def(DefKind::Static, ..));
  • 字段/下标等投影运算a.bv[i]),只要基础表达式本身是位置表达式,其投影结果仍是位置表达式;
  • 解引用 *p:可写的解引用结果对应的是被指向的那块内存;
  • 类型标注、unsafe 绑定转型等"透传"表达式的"位置性"继承自其操作数。

反过来,字面量、& 引用临时值、运算式、构造表达式、方法调用结果等都不满足条件,因此会触发本错误。

4.2 报错点一:复合赋值运算符检查 check_expr_assign_op

当编译器解析到形如 a += b 的表达式时,会调用 check_expr_assign_op。该函数位于 compiler/rustc_hir_typeck/src/op.rs(文件头注释 "Code related to processing overloaded binary and unary operators" 表明这是运算符处理模块):

pub(crate) fn check_expr_assign_op(
    &self,
    expr: &'tcx Expr<'tcx>,
    op: hir::AssignOp,
    lhs: &'tcx Expr<'tcx>,
    rhs: &'tcx Expr<'tcx>,
    expected: Expectation<'tcx>,
) -> Ty<'tcx> {
    let (lhs_ty, rhs_ty, _return_ty) =
        self.check_overloaded_binop(expr, lhs, rhs, Op::AssignOp(op), expected);
    // ... 内置二元运算类型约束检查 ...
    self.check_lhs_assignable(lhs, E0067, op.span, |err| { /* ... */ });
}

关键在最后一行:它把错误码 E0067 与左侧表达式 lhs 一起传给 check_lhs_assignable——凡是 +=-=*=/=%=&=|=^=<<=>>= 这类复合赋值运算符,左侧不可赋值时报的都是 E0067

4.3 报错点二:统一判定函数 check_lhs_assignable

check_lhs_assignable 实现在 compiler/rustc_hir_typeck/src/expr.rs,它先把要检查的代码号作为参数接收,再统一执行"是否可赋值"的判定:

pub(crate) fn check_lhs_assignable(
    &self,
    lhs: &'tcx hir::Expr<'tcx>,
    code: ErrCode,
    op_span: Span,
    adjust_err: impl FnOnce(&mut Diag<'_>),
) {
    if lhs.is_syntactic_place_expr() {
        return; // 是位置表达式,直接放行
    }
    // ... 对 let 链等特殊情况提前返回 ...
    let mut err = self.dcx().struct_span_err(op_span, "invalid left-hand side of assignment");
    err.code(code); // 写入 E0067 / E0070 等错误码
    err.span_label(lhs.span, "cannot assign to this expression");
    // ...
    err.emit();
}

这里可以看到两条重要的工程信息:

  1. 先判定后报错is_syntactic_place_expr 返回 true 就立即返回(放行),只有判定失败才会构造诊断信息;
  2. 错误码由调用方注入check_lhs_assignable 不硬编码错误码。复合赋值路径传入 E0067,而普通赋值(见 compiler/rustc_hir_typeck/src/demand.rs 中同为左侧判定服务的调用)则可能带 E0070。因此我们能在源码上确认:E0067 是"复合赋值运算符"专属的错误码,E0070 则负责普通 = 赋值

五、实战修复场景

除了原文档的"先 let mut 再赋值"这一标准解法,日常开发中 E0067 还有几个高频场景,分别对应不同的修复动作。

场景一:对方法调用结果复合赋值

fn main() {
    let mut v = vec![1, 2, 3];
    // v.first_mut().unwrap() += 1; // 这样写会触发 E0067
    *v.first_mut().unwrap() += 1; // ok:先解引用,得到可写位置
}

first_mut() 返回 Option<&mut i32>.unwrap() 得到的 &mut i32 本身只是引用值;要写成复合赋值左侧,必须用 * 解引用到被引用的 i32 存储槽。这也印证了 4.1 中"解引用结果仍是位置表达式"的规则。

场景二:对构造的临时对象复合赋值

use std::collections::LinkedList;

fn main() {
    let mut list = LinkedList::new();
    list.push_back(1);
    // list += ... // LinkedList 本身不实现 AddAssign
}

直接用 LinkedList::new() += 1; 这类"临时对象"写在左侧,会同时遭遇 E0368(类型未实现 AddAssign)与 E0067(左侧不可赋值)。仓库中的回归测试 tests/ui/error-codes/E0067.rs 正覆盖了这一组合场景:

use std::collections::LinkedList;

fn main() {
    LinkedList::new() += 1; //~ ERROR E0368
                            //~^ ERROR E0067
}

其期望输出文件 tests/ui/error-codes/E0067.stderr 显示,编译器会对同一行报两条错误,其中 E0067 的标注正是:

error[E0067]: invalid left-hand side of assignment
   |
LL |     LinkedList::new() += 1;
   |     ----------------- ^^
   |     |
   |     cannot assign to this expression

场景三:二元运算结果当左侧

fn main() {
    let mut a = 1;
    let mut b = 2;
    // (a + b) += 1; // error[E0067]:加法结果是无名临时值
    a += b;         // ok:读 a、加 b、写回 a
    // 若要同时改两个变量,应分开写:
    // a += 1;
    // b += 1;
}

a + b 的求值结果没有对应的存储槽位,不能作为复合赋值左侧。这是"把右侧语义误当作左侧"的典型笔误。

六、编译器给出的智能建议

值得一提的是,rustc 对 E0067 并非只会生硬报错。回看 compiler/rustc_hir_typeck/src/op.rs,在复合赋值场景中,报错逻辑会额外探测 *lhs 是否存在合法的运算符重载实现:

self.check_lhs_assignable(lhs, E0067, op.span, |err| {
    if let Some(lhs_deref_ty) = self.deref_once_mutably_for_diagnostic(lhs_ty) {
        if /* 检查 deref 一次后是否可行 */ {
            // ...
            err.span_suggestion_verbose(
                lhs.span.shrink_to_lo(),
                "consider dereferencing the left-hand side of this operation",
                "*", // 建议在左侧插入一个 *
                Applicability::MaybeIncorrect,
            );
        }
    }
    // ...
});

也就是说,当代码是 foo() += rhs*foo() += rhs 恰好合法(例如左侧解引用后类型实现了 AddAssign)时,编译器会主动建议在左侧加一个 * 解引用运算符。理解这条建议背后的机制,能帮助你在收到提示时快速判断它是否适用。

七、修复清单与自查要点

将以上内容汇总成可直接套用的自查清单:

  1. 左侧是不是字面量12 += 1)?→ 换成 mut 变量;
  2. 左侧是不是函数/方法调用、构造表达式或运算结果LinkedList::new() += 1)?→ 把它先存进 mut 变量,或对返回的 &mut 结果用 * 解引用后再复合赋值;
  3. 左侧变量是否声明为 mut?→ 未声明为 mut 时虽然不会报 E0067,但会触发 E0384 不可变赋值错误,同样需要补 mut
  4. 是不是普通 = 赋值报的 E0070?→ 它负责普通赋值路径,修复思路与 E0067 完全一致:把左侧换成位置表达式。

修复完成后,可以用 rustc 直接验证。在装有该版本工具链的终端里执行错误码查询:

rustc --explain E0067

该命令会打印与 compiler/rustc_error_codes/src/error_codes/E0067.md 同源的详细解释。error_codes 目录下每个 .md 文件与 rustc --explain <code> 一一对应,是查看全部错误码长解释的首选路径;相应的 UI 回归测试则集中在 tests/ui/error-codes/ 目录,若想阅读更多错误码的实际触发样例,可按文件名(如 E0067.rs + 同名 .stderr)检索。

结语

E0067 是 Rust 所有"赋值左侧不合法"类错误中最常见的一支。它的判断标准非常清晰——左侧必须是能代表存储位置的 place expression;触发它时,把右手边那个"算出来的值"先存进一个 mut 变量,或者对 &mut 结果做一次 * 解引用,问题通常就能迎刃而解。通过 错误码文档expr.rs 的 check_lhs_assignableop.rs 的 check_expr_assign_op 三处对照阅读,你已经能从"报错信息—判定逻辑—修复策略"三个层面完整理解这一类编译错误。

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

项目优选

收起
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