首页
/ 深入解读 Rust 编译错误 E0322:为何 `Sized` 等编译器内置 trait 禁止手动实现

深入解读 Rust 编译错误 E0322:为何 `Sized` 等编译器内置 trait 禁止手动实现

2026-09-07 16:25:28作者:宣海椒Queenly

E0322 是 rustc 在类型检查的 coherence(一致性)检查阶段抛出的错误:当你在源码中尝试显式 impl 一个由编译器完全自动提供的内置 trait(built-in trait)时被拒绝,典型触发点就是 impl Sized for Foo {}。本篇文章将以 rustc 错误码文档 E0322.md 为骨架,结合本仓库编译器(rustc_hir_analysis)与标准库(library/core)的源码实现,讲清该错误的触发场景、编译器拦截机制、同类 trait 的完整清单以及正确的规避/修复思路。

一、错误速览与最小复现

按照 rustc 错误码文档的官方定义,E0322 的含义是:

A built-in trait was implemented explicitly. All implementations of the trait are provided automatically by the compiler. (某个编译器内置 trait 被显式实现了。该 trait 的所有实现均由编译器自动提供。)

文档给出了最直接的错误示例:

struct Foo;

impl Sized for Foo {} // error!

这段代码在编译时会得到近似如下的诊断输出(消息文本取自本仓库 coherence 阶段的实现):

error[E0322]: explicit impls for the `Sized` trait are not permitted
  --> src/lib.rs:3:1
   |
3  | impl Sized for Foo {} // error!
   | ^^^^^^^^^^^^^^^^^^^^ impl of `Sized` not allowed

二、Sized 到底是什么:从 trait 定义看编译器内置语义

Sized 是标准库 library/core/src/marker.rs 中定义的一个空 marker trait,其定义前挂着一串极具信息量的内部属性:

#[lang = "sized"]
#[fundamental]
#[rustc_specialization_trait]
#[rustc_deny_explicit_impl]
#[rustc_dyn_incompatible_trait]
#[rustc_coinductive]
pub trait Sized: MetaSized {
    // Empty.
}

逐一解读这些属性即可还原该 trait 的“编译器内置”本质:

  • #[lang = "sized"]:通过 lang item 机制,Sized 在编译器内部被特殊识别,rustc 的诸多类型检查路径直接依赖这个 lang item 做布局与类型大小判断,而不是把它当普通用户 trait 处理;
  • #[rustc_deny_explicit_impl]:正是这个内部属性让 impl Sized for X 触发 E0322(见下一节源码分析),它表示“该 trait 的所有实现只能由编译器生成,禁止用户在源码中手写 impl 块”;
  • #[fundamental]:使得围绕 Sized 的某些泛型规则(如允许 [T]: !Sized 之类的推理)可以正常工作;
  • #[rustc_dyn_incompatible_trait]Sized 不能作为 trait object 的 principal trait(这符合直觉,DST 对象不能有编译期已知大小);
  • #[rustc_coinductive]:在 trait solver 中按共归纳(coinductive)方式求解,因为所有实现都是编译器内建的,不会出现用户自写 impl 导致的环。

谁实现了 Sized

Sized 的语义是“类型的字节大小在编译期已知且恒定”。它由编译器按需为所有满足条件的类型自动生成实现,例如:

  • 所有标量类型、&T / &mut T 引用(无论 T 是否有大小);
  • 定长数组 [T; N](其中 T: Sized);
  • 字段全部有确定大小的结构体、枚举与元组。

相反,**动态尺寸类型(DST)**没有实现 Sized:典型的如切片 [T](注意区别于数组 [T; N])、str、trait 对象 dyn Trait,它们的大小只有运行时才能从“指针 + 元数据”得知。

三、编译器如何拦截:coherence 阶段的 enforce_trait_manually_implementable

错误码只是“表象”,真正实施拦截的是 rustc 的 coherence(一致性)检查。在类型检查中,coherence 阶段的职责是保证每个 trait 对每种类型至多只有一个 impl(由 orphan/overlap 检查实现),同时也会校验 impl 的合法性。

该阶段的入口文件是 compiler/rustc_hir_analysis/src/coherence/mod.rs,其中 check_impl 依次执行三类校验:

enforce_trait_manually_implementable(tcx, impl_def_id, trait_ref.def_id, trait_def)
    .and(enforce_empty_impls_for_marker_traits(tcx, impl_def_id, trait_ref.def_id, trait_def))
    .and(always_applicable::check_negative_auto_trait_impl(tcx, impl_def_id, trait_ref, polarity))

E0322 就产生于第一个校验函数 enforce_trait_manually_implementablemod.rs#L54-L114):

// Disallow *all* explicit impls of traits marked `#[rustc_deny_explicit_impl]`
if trait_def.deny_explicit_impl {
    let trait_name = tcx.item_name(trait_def_id);
    let mut err = struct_span_code_err!(
        tcx.dcx(),
        impl_header_span,
        E0322,
        "explicit impls for the `{trait_name}` trait are not permitted"
    );
    err.span_label(impl_header_span, format!("impl of `{trait_name}` not allowed"));
    ...
    return Err(err.emit());
}

可见核心判定条件只有一条:该 trait 的 TraitDef 是否带有 deny_explicit_impl 标记。而这个标记来自 trait 定义上的 #[rustc_deny_explicit_impl] 内部属性,它由 rustc 在收集 trait 定义时解析并写入 trait def 结构体,相关解析代码在 compiler/rustc_hir_analysis/src/collect.rs#L1149-L1167

四、并非所有内置 trait 都报 E0322:三个特殊分支

如果将所有“编译器内置 trait 的手写 impl”都一律报 E0322,用户诊断体验会很差。因此上述函数对几个特殊 trait 做了差异化处理,这些分支对我们理解“哪些 trait 能手动 impl、哪些不能”非常关键:

  1. Freeze 走 feature 门控而非 E0322mod.rs#L62-L71):

    if tcx.is_lang_item(trait_def_id, LangItem::Freeze) && !tcx.features().freeze_impls() {
        feature_err(..., "explicit impls for the `Freeze` trait are not permitted", ...)
    }
    

    注意标准库中 marker.rs#L902 还留有一条 FIXME,说明 Freeze 目前尚未标记 #[rustc_deny_explicit_impl],只是暂时通过 freeze_impls feature 限制,未来会逐步迁移为统一的 E0322 机制。

  2. Unsize 单独报 E0328mod.rs#L84-L90):

    // Maintain explicit error code for `Unsize`, since it has a useful
    // explanation about using `CoerceUnsized` instead.
    if tcx.is_lang_item(trait_def_id, LangItem::Unsize) {
        err.code(E0328);
    }
    

    因为 Unsize 有专门的用户指引(改用 CoerceUnsized 处理自定义容器与 DST 之间的强制转换),所以 rustc 特意为它保留了 E0328 这个独立错误码,而不是套用笼统的 E0322。

  3. marker trait 若带关联项则报 E0715enforce_empty_impls_for_marker_traitsmod.rs#L118-L139)允许 marker trait 的 impl 之间相互重叠,因此若 impl 中携带了关联项会造成歧义,会以 E0715 拒绝。

五、完整名单:哪些 trait 属于“禁止显式实现”范畴

从源码结构看,#[rustc_deny_explicit_impl] 目前主要用于描述语言语义本身就由编译器实现、无法用普通 impl 表达的 trait。可以据此判断 E0322 的适用范围正在扩大,而不仅仅限于 Sized

以标准库 library/core/src/marker.rs 为例,被标记的 trait 包括:

Trait 位置 编译器自动提供的能力
Sized marker.rs#L152 类型大小编译期已知,按布局自动判定
MetaSized / PointeeSized marker.rs#L171 / #L188 sized_hierarchy 实验分层中的大小语义(Pointee 元数据相关)
Unsize<T> marker.rs#L233 [T; N] -> [T]T -> dyn Trait、结构体末字段递归等内置“变瘦”转换
BikeshedGuaranteedNoDrop marker.rs#L506 判定类型可放入 union 字段(结合 Copy 递归推导)
DiscriminantKind marker.rs#L881 枚举判别式类型,mem::Discriminant 的底层支撑
Destruct marker.rs#L1061 标示类型可在编译期/运行期析构,供 [const] 泛型约束使用
Tuple marker.rs#L1073 仅元组类型实现的内建标记

此外在 core 的其他模块还能看到同类 trait,例如 library/core/src/any.rs#L962TryAsDynCompatible<'a>(支撑 try_as_dyn 实验特性,编译器负责为合适的 dyn Trait 自动实现)、library/core/src/field.rs#L148Field(field projections 实验特性中编译器为“字段代表类型”生成实现)、library/core/src/ops/function.rs#L326-L328FnPtr(所有函数指针自动实现),以及 mem/transmutability.rsptr/metadata.rs 中与 transmutePointee 元数据相关的内部 trait。

一个共同特征是:这些 trait 大多同时拥有 #[lang = "..."] 标记与(大部分)#[rustc_dyn_incompatible_trait] 标记,其实现逻辑与类型布局、指针元数据、函数调用约定等编译器内建概念深度耦合,无法用 Rust 语言自身的 impl 语法忠实表达。

六、为什么必须禁止:设计动机与一致性考量

理解 E0322 的价值在于弄懂“编译器为什么不让你省事”。从本仓库的源码结构与语言设计看,主要有四点:

  1. 实现会破坏一致性(coherence):coherence 要求“同一 trait + 同一自类型”至多一个 impl。如果 Sized 可由用户声明,用户写 impl Sized for Foo 之后,一旦编译器又因 Foo 实际有确定大小而自动生成实现,就会出现重叠;若编译器因信任该声明而放弃自检,又会打开 UB 之门——因为 Sized 直接决定值是否可“按值移动、按栈上布局存放”。
  2. 语义无法由用户正确表达Sized 的实现条件是类型是否“在编译期已知大小”,这需要完整的布局计算(含 repr 规则、泛型参数等)。impl Sized for Foo 没有任何可写的关联项或方法体,它是一个“编译器根据类型结构自动判定为真”的命题,不属于用户可主张的范畴。
  3. 特殊化与 trait solver 的推理前提Sized 被标记为 #[rustc_coinductive]#[rustc_specialization_trait](见 marker.rs#L151-L157 注释),这些机制都建立在“不存在用户手写 impl”的前提之上——marker.rs 中有一段注释明确写道:“Sized 之所以即使有父 trait 也可以共归纳求解,是因为没有任何用户手写的 impl,且只需查看内建 impl 即可确定父 trait 一定被实现”。
  4. 避免误导性的 trait object / DST 行为Sized 还是决定泛型默认约束的关键——Rust 中每个泛型类型参数默认都带隐式的 T: Sized,除非显式写 ?Sized。若允许用户冒充实现,整个泛型默认规则与类型推导都会失真。

七、实际遇到 E0322 时该怎么办

E0322 属于“编译期即可绕开”的错误,rustc 文档给出的处理原则是:删除该显式 impl,让编译器自动完成工作。结合 Sized 的使用场景,实践中常见三种情况:

1. 想给自定义类型“声明”它是有大小的(最常见的误用)

struct Foo;

impl Sized for Foo {} // error! Foo 本身就有确定大小,编译器已自动实现 Sized

直接删除这段 impl 即可。注意:默认情况下所有具体类型都是有大小的impl Sized 毫无必要。如果你需要表达“此泛型参数必须有大小”的约束,直接依赖默认行为即可:

// T 默认已隐含 T: Sized,无需任何额外声明
fn requires_sized<T>(_value: T) {}

2. 误以为“去掉 Sized 约束”会导致类型不合法

有时开发者会写出形如 impl Sized for Foo 来“让编译器满意”,实际根因往往是别处的约束写错。正确的处理是保留类型的 DST 属性,并在需要按值操作的地方通过引用/指针间接使用:

// 若你想让类型能够容纳一个“未定大小”的字段,应通过间接层而非伪造 Sized
struct Wrapper<T: ?Sized> {
    inner: Box<T>,   // DST 必须放在指针等间接层后面
}

3. 需要实现 Unsize/CoerceUnsized 这类相关 trait

若你的目标是让 Foo<T> 支持把最后一个泛型字段“变瘦”为 DST(类似 Rc<[T]> 那样),正确入口是 CoerceUnsized 而不是 Unsize——这也正是编译器对 Unsize 单独报 E0328(而非 E0322)并附上 CoerceUnsized 提示的原因(见 mod.rs#L84-L88)。

八、延伸阅读:在仓库中继续验证

如果你希望进一步从源码层面吃透这条错误链,可按如下路径在本仓库内继续追踪:

  1. 错误码原始文档:compiler/rustc_error_codes/src/error_codes/E0322.md——官方给出的最小复现与语义说明,是本文第一手依据;
  2. 诊断发射点:compiler/rustc_hir_analysis/src/coherence/mod.rs#L54-L114——enforce_trait_manually_implementable 是 E0322 的唯一发射处,可看到 Freeze/Unsize 等特殊分支;
  3. 属性解析:compiler/rustc_hir_analysis/src/collect.rs#L1149-L1167——#[rustc_deny_explicit_impl] 如何被收集进 TraitDef
  4. trait 本体:library/core/src/marker.rsSized(约 L143-L160)、UnsizeDiscriminantKind 等定义,以及 library/core/src/ops/function.rs#L322-L329FnPtr,观察它们共同的 #[rustc_deny_explicit_impl] 与 lang item 属性组合。

总的来说,E0322 传达的是一条清晰的边界:凡是“语言机制本身”的 trait,编译器不会把实现权交给用户。理解这条边界后,当你在未来看到 E0328、E0715 等同族诊断,或遇到 Freeze 等新特性被同样机制约束时,就能快速定位到同一套 coherence 校验逻辑。

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