Rust 编译器错误 E0161 详解:为什么不能移动未知大小的值(dyn 类型)及解决方法
E0161(cannot move a value of type ...)是 Rust 编译器在类型检查阶段报出的经典错误,触发原因是你试图"移动"(move)一个在编译期无法确定大小(unsized)的值,例如裸的 dyn Trait 对象。读完本文,你将理解该错误背后的 Sized 规则与 MIR 层面的检查机制,掌握通过引用、Box、Rc 等方式绕过限制的完整方案,并能定位到 rustc 源码中产生该诊断的具体位置。
E0161 错误是什么
E0161 的完整诊断信息形如:
error[E0161]: cannot move a value of type `dyn Bar`
--> src/main.rs:6:5
|
LL | b.f();
| ^ the size of `dyn Bar` cannot be statically determined
官方错误文档对该错误的定义是:尝试移动一个在编译期大小未知的值(a value was moved whose size was not known at compile time)。在 Rust 中,只有当类型在编译期大小已知(即实现了 Sized)时,值才能被直接移动、赋值或按值传递;dyn Trait、切片 [T]、C 风格字符串 str 这类动态大小类型不满足该前提。
触发错误的典型代码
官方错误文档 E0161.md 给出的原始示例是:
trait Bar {
fn f(self); // 按值消费 self
}
impl Bar for i32 {
fn f(self) {}
}
fn main() {
let b: Box<dyn Bar> = Box::new(0i32);
b.f();
// error[E0161]: cannot move a value of type `dyn Bar`:
// the size of `dyn Bar` cannot be statically determined
}
这里的关键在于方法 f 的接收者是 self(按值移动)。当你调用 b.f() 时,编译器需要将 b 解引用后按值移动 dyn Bar 这个本体。虽然 Box<dyn Bar> 本身是定长的(一个指针),但被移动的值类型是 dyn Bar 而非 Box<dyn Bar>——dyn Bar 的实际数据大小在编译期不可知,因此按值移动它无法生成合法代码,编译器直接报 E0161。
仓库中的 UI 测试 E0161.rs 验证了同一场景,并特意注释说明这是为了确认 E0161 在任何可能影响它的配置下都是硬错误:
// Check that E0161 is a hard error in all possible configurations that might
// affect it.
#![crate_type = "lib"]
trait Bar {
fn f(self);
}
fn foo(x: Box<dyn Bar>) {
x.f();
//~^ ERROR E0161
}
对应的期望诊断输出见 E0161.stderr:
error[E0161]: cannot move a value of type `dyn Bar`
--> $DIR/E0161.rs:11:5
|
LL | x.f();
| ^ the size of `dyn Bar` cannot be statically determined
注意错误 span 指向的是调用语句 x.f(); 整体,而不是 dyn 类型出现的位置——这一点与源码中的检查点位置一致,下文会说明。
官方推荐的修复方案:把值藏在引用后面
错误文档给出的解决方法是:"将值隐藏在引用后面"——使用 &x 或 &mut x。引用的大小是固定的(普通指针 8 字节、胖指针 16 字节),因此可以像普通值一样自由移动和传递。具体做法是把 trait 方法的接收者从 self 改为 &self:
trait Bar {
fn f(&self); // 改为借用 self
}
impl Bar for i32 {
fn f(&self) {}
}
fn main() {
let b: Box<dyn Bar> = Box::new(0i32);
b.f(); // ok!
}
此时 b.f() 实际执行的是 Bar::f(&*b):先解引用得到 dyn Bar 的借用,再传入定长的引用,全程没有移动 dyn Bar 本体,编译通过。
如果方法确实需要消费对象(比如要取出内部所有权),正确姿势是移动"指针"而不是移动"本体":
trait Bar {
fn f(self); // 保持按值语义
}
impl Bar for i32 {
fn f(self) {}
}
fn main() {
let b: Box<dyn Bar> = Box::new(0i32);
Box::into_inner(b).f(); // 移动 Box(定长指针),而不是 dyn Bar
// 或者把参数类型声明为 Box<dyn Bar>,由调用方负责解包
}
同理,函数参数、返回值中也不能直接出现裸的 dyn Trait(这通常是另一个更早期的 unsized type 相关报错),应写成 &dyn Trait、Box<dyn Trait>、Rc<dyn Trait> 等定长包装形式。
源码级解析:E0161 在哪里、如何产生
E0161 的诊断结构体定义在 session_diagnostics.rs:
#[derive(Diagnostic)]
#[diag("cannot move a value of type `{$ty}`", code = E0161)]
pub(crate) struct MoveUnsized<'tcx> {
pub ty: Ty<'tcx>,
#[primary_span]
#[label("the size of `{$ty}` cannot be statically determined")]
pub span: Span,
}
两个 diag/label 字符串正好对应你在终端看到的"主错误行 + ^ 下划线处的补充说明",与 E0161.stderr 中的输出逐字一致。
真正触发该诊断的是 MIR 类型检查中的 ensure_place_sized 方法,位于 type_check/mod.rs:
fn ensure_place_sized(&mut self, ty: Ty<'tcx>, span: Span) {
let tcx = self.tcx();
// Erase the regions from `ty` to get a global type. ...
let erased_ty = tcx.erase_and_anonymize_regions(ty);
// FIXME(#132279): Using `Ty::is_sized` causes us to incorrectly handle opaques here.
if !erased_ty.is_sized(tcx, self.infcx.typing_env(self.infcx.param_env)) {
// in current MIR construction, all non-control-flow rvalue
// expressions evaluate through `as_temp` or `into` a return
// slot or local, so to find all unsized rvalues it is enough
// to check all temps, return slots and locals.
if self.reported_errors.replace((ty, span)).is_none() {
// While this is located in `nll::typeck` this error is not
// an NLL error, it's a required check to prevent creation
// of unsized rvalues in a call expression.
self.tcx().dcx().emit_err(MoveUnsized { ty, span });
}
}
}
从源码结构看,检查逻辑包含三个要点:
- 判据是
Sized而非字面"大小":先把类型中的区域(lifetime)擦除得到全局类型(Sized判定与精确的 lifetime 无关),然后调用is_sized判断。只要类型不满足Sized,且该类型出现在一个"值被求值/存放"的位置(临时变量、返回值槽、局部变量),就会报 E0161。 - 检查点覆盖所有临时对象(temps):同文件上方 type_check/mod.rs 中,遍历
LocalKind::Temp声明时逐一调用ensure_place_sized。源码注释解释了为什么这样做就够:当前 MIR 构造中,所有非控制流的 rvalue 表达式都会经过as_temp或求值进返回槽/局部变量,所以只要检查所有 temps、返回槽和局部变量,就能覆盖全部 unsized rvalue——b.f()这种按值调用生成的临时求值对象正落入此列。 - 同一位置只报一次:
reported_errors.replace(...)的去重机制保证同一处 unsized 类型不会重复刷屏,且注释明确指出该检查虽然位于 borrowck(NLL 类型检查)中,但本质上是防止"在调用表达式中创建 unsized rvalue"的必备检查,不属于借用检查规则本身。
这解释了测试输出中 span 指向整条调用语句的现象:错误 span 来自生成该临时对象的 MIR 位置信息(local_decl.source_info.span),而非类型标注处。
适用前提与限制
- 本错误是硬错误,不依赖任何 feature gate;E0161.rs 的注释表明团队专门验证了它在各种可能相关的配置下(如不同
crate_type)始终成立。 - 源码中
ensure_place_sized存在一个已知的 FIXME(引用问题 #132279):当前实现对 opaque 类型(如impl Trait背后的类型)的is_sized处理尚不完美,可以推断这类边缘情况下的诊断行为可能随编译器版本演进。 - 当 unstable feature
unsized_fn_params启用时,对参数位置的检查会转移到终止符(terminator)路径上(见 type_check/mod.rs 中if self.tcx().features().unsized_fn_params()分支),但在稳定版行为不受影响。
总结
| 项目 | 内容 |
|---|---|
| 错误码 | E0161 |
| 触发条件 | 尝试移动/求值一个不满足 Sized 的值(如裸 dyn Trait、[T]、str) |
| 诊断来源 | MoveUnsized,由 ensure_place_sized 在 MIR 类型检查中发出 |
| 首选修复 | 将 self 方法改为 &self(或 &mut self),通过引用传递 |
| 需要消费时 | 传递/解包定长智能指针,如 Box<dyn Trait>、Rc<dyn Trait>,而不是裸 dyn Trait |
| 参考测试 | tests/ui/error-codes/E0161.rs、E0161.stderr |
核心记忆点一句话:Rust 只允许移动编译期大小已知的值;遇到 E0161 时,把"移动本体"改为"移动指向本体的定长指针(引用或智能指针)"即可。
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