
rustc 编译错误 E0377 深入解析CoerceUnsized 与 DispatchFromDyn 为何只能作用于同一结构定义【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust本文基于本仓库 rustc 编译器的错误码文档与相关实现源码系统拆解编译错误 E0377CoerceUnsized/DispatchFromDyn只能在相同结构定义之间实现。你将理解该错误出现的两种形态、底层涉及的“不定长强转unsized coercion”与 trait 对象分发机制掌握如何编写合法的手动CoerceUnsized实现并沿着编译器源码定位到错误的真实发出位置学会如何正确阅读与修复此类错误。E0377 的权威解释文档位于 compiler/rustc_error_codes/src/error_codes/E0377.md本文的全部分析与代码示例均以当前仓库实际内容为准。一、错误码 E0377 的含义与触发场景E0377 的错误信息是the trait CoerceUnsized may only be implemented for a coercion between structures with the same definition其含义是程序员为类型实现了CoerceUnsized或DispatchFromDyntrait但源类型与目标类型并不是同一个结构体定义。编译器无法支持两个不同结构体类型之间的此类强转因此合法的实现必须是同一个结构体、仅仅替换泛型参数。需要说明的是当前仓库编译器源码中该错误的实际诊断文案是“the trait{$trait_name}may only be implemented for a coercion between structures”其中trait_name会随被检查的 impl 而变CoerceUnsized或DispatchFromDyn因此你编译时看到的报错可能在两个 trait 名下出现。一个必然触发 E0377 的示例下面是文档中给出的完整错误示例compile_fail即预期编译失败#![feature(coerce_unsized)] use std::ops::CoerceUnsized; pub struct FooT: ?Sized { field_with_unsized_type: T, } pub struct BarT: ?Sized { field_with_unsized_type: T, } // error: the trait CoerceUnsized may only be implemented for a coercion // between structures with the same definition implT, U CoerceUnsizedBarU for FooT where T: CoerceUnsizedU {}问题出在最后一行这里试图声明FooT可以强转为另一个结构体BarU。虽然Foo与Bar的字段布局看起来完全相同但它们是两个不同的类型定义编译器无从推导Foo各字段到Bar各字段之间如何“逐一对应地变宽指针”因此拒绝接受这个实现并报告 E0377。二、认识两个被实现的不稳定 trait1.CoerceUnsized让“自定义盒子”可以被不定长强转CoerceUnsized位于 library/core/src/ops/unsize.rspub trait CoerceUnsizedT: PointeeSized: Sized { // Empty. }它的作用是标记本类型可以被强转coerce为另一个具有相同结构、但携带“可不定长化字段”的目标类型。典型场景是自定义智能指针内建指针已内建支持a mut T可强转为a mut dyn Trait需要T: Unsizedyn Trait自定义的MyBoxT也希望通过一条手动 impl 获得同样的能力让MyBoxT强转成MyBoxdyn Trait。在核心库中标准库为引用与裸指针提供了一组基础实现例如 library/core/src/ops/unsize.rs#[unstable(feature coerce_unsized, issue 18598)] impla, b: a, T: PointeeSized UnsizeU, U: PointeeSized CoerceUnsizeda mut U for b mut T { }Unsize由编译器内建驱动[i32; 3]: Unsize[i32]、T: Unsizedyn Trait等都由 rustc 直接生成用户不能为Unsize手动写 impl。2.DispatchFromDyn支持Boxdyn Trait以值方式调用 trait 方法同文件继续向下看pub trait DispatchFromDynT: Sized { // Empty. }DispatchFromDyn用于dyn 兼容 trait 的虚表分发当你在 trait 对象dyn Trait上以self: MyBoxSelf的形式接收Self时编译器需要从MyBoxdyn Trait的宽指针数据指针 vtable转换回MyBoxSelf才能找到具体的Self类型进行方法分发。DispatchFromDyn就是这个“把盒内指向 trait 对象的指针还原为指向具体Self的指针”的通行证。核心库同样为其提供了引用与裸指针实现例如impla, T: PointeeSized UnsizeU, U: PointeeSized DispatchFromDyna U for a T {} impla, T: PointeeSized UnsizeU, U: PointeeSized DispatchFromDyna mut U for a mut T {} implT: PointeeSized UnsizeU, U: PointeeSized DispatchFromDyn*const U for *const T {} implT: PointeeSized UnsizeU, U: PointeeSized DispatchFromDyn*mut U for *mut T {}正因Box、Rc、Arc等智能指针实现了CoerceUnsized与DispatchFromDyn文档明确指出这两个 trait 主要服务这类智能指针我们才能写出let b: Boxdyn Trait Box::new(concrete);这样的代码并能以Boxdyn Trait作为方法接收者完成虚调用。三、修复方式同一结构体、只换泛型参数既然Foo与Bar是两个不同定义自然无法互相强转。要让上述“泛型结构体随类型参数一起变宽”的意图成立正确写法是把 impl 写回同一个结构体上#![feature(coerce_unsized)] use std::ops::CoerceUnsized; pub struct FooT: ?Sized { field_with_unsized_type: T, } // OK同一结构 Foo只是泛型参数从 T 换成了 U implT, U CoerceUnsizedFooU for FooT where T: CoerceUnsizedU {}如果把T与U理解为指针类的类型如b mut V与a mut W该 impl 表达的就是“Foo指向的内容发生了 unsized 强转如V: UnsizeW因此整个Foo也可以跟着强转”。这与文档中的结论完全一致CoerceUnsized用于强转那些含有一个“可以被不定长化字段”的结构体例如自定义MyBoxT被强转为MyBoxdyn TraitDispatchFromDyn则用于在 dyn 兼容 trait 中把MyBoxdyn Trait分发到MyBoxSelf。编译器不支持不同类型结构体之间的强转因此一个合法的实现应写在同一结构体、不同泛型参数之间。四、从源码看编译器究竟如何裁决E0377 的发出点E0377 的诊断结构体定义在 compiler/rustc_hir_analysis/src/diagnostics.rs共有两个同 code 的诊断#[derive(Diagnostic)] #[diag(the trait {$trait_name} may only be implemented for a coercion between structures, code E0377)] pub(crate) struct CoerceUnsizedNonStruct { #[primary_span] pub span: Span, pub trait_name: static str, }以及带注释的版本#[derive(Diagnostic)] #[diag(the trait {$trait_name} may only be implemented for a coercion between structures, code E0377)] pub(crate) struct CoerceSameStruct { #[primary_span] pub span: Span, pub trait_name: static str, #[note( expected coercion between the same definition; expected {$source_path}, found {$target_path} )] pub note: bool, pub source_path: String, pub target_path: String, }从诊断体命名可以推断出 E0377 覆盖两种情形CoerceUnsizedNonStruct源类型或目标类型根本不是结构体例如 enum、union 等时报告CoerceSameStruct两侧虽然都是结构体但属于不同定义时报告并附注“expected coercion between the same definition; expectedFoo, foundBar”以帮助定位。实际的裁决逻辑coerce_unsized_info真正做裁决的是查询coerce_unsized_info声明于 compiler/rustc_middle/src/queries.rs/// Caches CoerceUnsized kinds for impls on custom types. query coerce_unsized_info(key: DefId) - Resultty::adjustment::CoerceUnsizedInfo, ErrorGuaranteed { desc { computing CoerceUnsized info for {}, tcx.def_path_str(key) } cache_on_disk separate_provide_extern }其实现位于 compiler/rustc_hir_analysis/src/coherence/builtin.rs。函数首先取出 impl 的Self类型source与目标类型target然后对两者形状做匹配。从源码可见合法形状对是A → B、A → *B、*A → *B引用与裸指针之间的宽化S... → S...其中两侧必须是同一结构体 ADTpattern typety::Pat之间则要求 pattern 相同。其中与 E0377 直接相关的就是结构体分支builtin.rs(ty::Adt(def_a, args_a), ty::Adt(def_b, args_b)) if def_a.is_struct() def_b.is_struct() { if def_a ! def_b { let source_path tcx.def_path_str(def_a.did()); let target_path tcx.def_path_str(def_b.did()); return Err(tcx.dcx().emit_err(diagnostics::CoerceSameStruct { span, trait_name, note: true, source_path, target_path, })); } // ... 同一结构体继续比较各字段 }def_a ! def_b即“两个不同定义的结构体”这正是 E0377 的发出点。若匹配不到任何合法形状目标不是结构体则由CoerceUnsizedNonStruct报告同一条错误。同一结构体分支里还做了什么当两侧是同一结构体后编译器会逐字段比较builtin.rs忽略PhantomData字段其类型不被视作真正的数据变化忽略类型没有变化的字段源码注释提到这里要求严格相等而非依赖型变/子类型是为了能在不计算 variance、不约束 opaque 类型的前提下完成检查相关讨论见源码注释引用的 #41936收集所有“发生显著变化的字段”随后继续验证这些字段自身是否能够完成潜在多层的unsized 强转并最终确认只存在一个携带元数据的可宽化尾部字段。这也解释了文档中where T: CoerceUnsizedU的意义整条 impl 是否成立递归地取决于那个发生变化的字段T能否强转到U。类型检查阶段的验证入口当类型检查器需要为一个表达式做 unsized coercion 时会走 compiler/rustc_hir_typeck/src/coercion.rs 的coerce_unsized流程它先构造Source: CoerceUnsizedTarget谓词再通过求解器选择用户自定义 impl。选中本地用户 impl 后coercion.rs会额外调用if impl_source.impl_def_id.is_local() let Err(guar) self.tcx.ensure_result().coerce_unsized_info(impl_source.impl_def_id) { self.fcx.set_tainted_by_errors(guar); }也就是说不合法含 E0377的手动 impl 会导致整个函数体被标记为“被错误污染”从而在代码生成阶段避免对这类非法强转求值防止更下游的崩溃源码注释明确说明某些不一致的CoerceUnsized实现可能引发 ICE。这也是为何 E0377 通常在类型检查阶段就报出并阻断后续流程。下游如何使用合法 impl 的信息当合法 impl 通过验证后coerce_unsized_info会返回CoerceUnsizedInfo { custom_kind }其中custom_kind记录了“哪个字段、如何变宽”的信息。单态化阶段在 compiler/rustc_monomorphize/src/lib.rs 中取出该custom_kind代码生成阶段则由 compiler/rustc_codegen_ssa/src/base.rs 的coerce_unsized_into据此把“瘦指针 元数据”组装成最终的宽指针。可见 E0377 这道校验本质上是为代码生成阶段“如何构造 fat pointer”提前背书——形状不一致的强转无法被翻译成任何有意义的机器表示因此必须在语义上被拒绝。五、与相邻错误码 E0376 的关系仓库中还保留了 compiler/rustc_error_codes/src/error_codes/E0376.md其正文标注“this error code is no longer emitted by the compiler”其历史示例正是“CoerceUnsized或DispatchFromDyn被实现在两个非结构体类型之间”。在当前的诊断实现中这类“非结构体”情形与“不同定义结构体”情形统一收敛到 E0377 这一个错误码之下由CoerceUnsizedNonStruct/CoerceSameStruct两个诊断结构体分别承载。因此在阅读旧资料或迁移旧代码时见到 E0376 的相关描述不必困惑——以当前编译器的 E0377 为准即可。六、小结如何排查与修复 E0377读报错的主体与附注若附注包含 “expected coercion between the same definition; expectedFoo, foundBar”说明你把 impl 写在了两个不同结构体之间若没有该附注多半是目标类型根本不是结构体。把 impl 目标改回同一个结构体implT, U CoerceUnsizedFooU for FooT对DispatchFromDyn同理。检查发生变化的字段只能有一个真实参与 unsized 的“尾部字段”且它自身必须满足对应的CoerceUnsized/Unsize约束PhantomData与类型未变的字段都会被忽略。回顾调用链Box/Rc/Arc等智能指针之所以无需用户干预即可实现T → dyn Trait的强转与虚表分发正是因为在 library/core/src/ops/unsize.rs 定义了CoerceUnsized/DispatchFromDyn两个 marker trait并由 compiler/rustc_hir_analysis/src/coherence/builtin.rs 的coerce_unsized_info对每个自定义 impl 做结构合法性把关。对需要自行实现“可不定长化自定义容器”自定义Box、零拷贝句柄、窄化指针包装等的开发者而言E0377 是最先遇到、也最值得彻底理解的一道约束它不是在限制你实现新类型而是在强制你把强转语义收敛在“同一结构、仅换参数”的安全边界之内。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考