Rust 编译器错误 E0161 详解:为什么不能移动未知大小的值(dyn 类型)及解决方法
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
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 时,把"移动本体"改为"移动指向本体的定长指针(引用或智能指针)"即可。
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考