☰
rustc 错误码 E0073 深度解析:递归结构体为何“造不出第一个实例”,以及 Option 如何终止递归
2026/11/2 7:53:13 网站建设 项目流程

rustc 错误码 E0073 深度解析:递归结构体为何“造不出第一个实例”,以及 Option 如何终止递归

【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust

导读

本文围绕 rust 编译器(rustc)中一个已经停止使用的历史错误码E0073展开,讲解它针对的递归结构体问题:当定义一个结构体Foo,其字段要求必须先拥有一个Foo实例才能构造新的Foo时,将永远不存在“第一个实例”,递归无法在值层面落地。文章会从该错误码的原始说明出发,还原问题成因、修复手法,并结合当前仓库中仍在使用中的相邻错误码 E0072 与编译器实现源码,理清“递归类型错误”这一家族的前世今生。读完后,你将能准确区分“类型无限大小”与“值无法构造”两类递归陷阱,并掌握用Option、Box与枚举基例设计可实例化递归数据结构的实战套路。

一、E0073 的错误码档案:一段已停用的诊断

E0073 的解释文档位于 compiler/rustc_error_codes/src/error_codes/E0073.md,其第一行就给出明确状态声明:

Note: this error code is no longer emitted by the compiler.

也就是说,E0073 在当前 rustc 中已经不再被触发了,该文档属于“保留编号 + 归档说明”性质的历史资料。

与此同时,0073这个编号仍然登记在 rustc 错误码总注册表中,见 compiler/rustc_error_codes/src/lib.rs 中error_codes!宏展开列表的0073一项。该文件头部注释明确规定了维护纪律(见 lib.rs 第 21~24 行):

Donotremove entries from this list. Instead, just add a note to the corresponding markdown file saying that this error is not emitted by the compiler any more … and remove all code examples that do not build any more by marking them withignore (no longer emitted).

这套约定保证了错误码文档永远可以按编号检索、又不会误导读者以为代码仍会触发该诊断。此外,该注释还提到错误码文档需遵循 RFC 1567 的格式规范,且宏内容会被 tidy 子系统的check_error_codes_docs检查(见 lib.rs 第 12~19 行)。

从源码检索情况看,E0073这一字面量在 compiler 目录内仅存在于解释文档与注册表,没有任何实际报错路径引用它——这与“no longer emitted”的标注相互印证。结论:E0073 是 rustc 历史版本中的“递归类型构造性检查”,如今已被新的递归类型诊断体系取代。

二、原始问题还原:永远造不出“第一个实例”

E0073.md 对问题本质的描述非常凝练:

You cannot define a struct (or enum)Foothat requires an instance ofFooin order to make a newFoovalue. This is because there would be no way a first instance ofFoocould be made to initialize another instance!

即:若构造Foo的某个环节必须已经存在一个Foo,那么整个过程没有起点。

原始文档给出的报错示例:

struct Foo { x: Box<Foo> } // error

初看之下这个结构体似乎没有问题——Box<Foo>是一个指针,尺寸已知(即 E0072 关注的“无限大小”并不成立)。但问题出在值的生产链上:

  1. 想要构造一个Foo,必须先准备字段x,也就是一个Box<Foo>;
  2. 一个合法的Box<Foo>必须指向某个已存在的Foo实例;
  3. 于是又回到第 1 步——需要一个Foo才能制造Foo。

这是一条在值层面无限循环、且不存在基例(base case)的生产依赖:任何一次尝试初始化都会向“更内层”请求又一个Foo,永无终点。类型系统在编译期无法凭空给你一个初始种子,因此该类型的任何值都无法被构造出来,属于“类型可表示、但集合为空”的构造性递归错误。

用类型构造器推理的视角看:Foo的可构造性方程是Foo = Box<Foo>,方程在“值存在性”意义上无解;必须在递归中出现一个不依赖Foo的分支才能“落地”。

三、与 E0072 的本质区别:无限大小 vs 无法构造

要真正理解 E0073,必须先分清它与现行错误码 E0072 是两个维度的问题:

维度E0072(现行,仍会触发)E0073(历史,已停用)
关注对象类型的尺寸/布局是否收敛类型是否存在可构造的基例
触发示例struct ListNode { head: u8, tail: Option<ListNode> }——tail直接内嵌ListNode,无指针间接层struct Foo { x: Box<Foo> }——已用指针,但无法产生首个Foo
失败原因求 size 时无限递归,尺寸不收敛求值时依赖自己,无初始实例
解决思路引入间接层(Box、&、Rc)打破尺寸递归引入可选/可空基例(Option、枚举空变体)终止值递归

E0072 文档用尺寸方程直观展示了前者的问题(见 E0072.md 第 22~26 行):

size of `ListNode` = 1 byte for `head` + 1 byte for the discriminant of the `Option` + size of `ListNode`

tail: Option<ListNode>让结构体尺寸方程在右边再次出现自身,编译期无法算出确定的字节数,因此必须通过Box之类指针把递归藏到间接层之后。而 E0073 场景中x: Box<Foo>已经把尺寸问题解决了,却仍未给出“停止递归、直接制造一个Foo”的途径——两种错误互为表里,共同构成 rustc 对递归 ADT 的完整防线。

现行编译器在处理递归类型的诊断时,把“递归但缺间接层”统一收敛到 E0072。在 compiler/rustc_query_impl/src/handle_cycle_error.rs 的报错路径中可以看到(第 282~296 行),编译器会输出recursive type ... infinite size(E0072),对每个参与循环的字段贴上recursive without indirection标签,并给出多片段建议:把字段包裹为Box<...>,提示文案为insert some indirection (e.g., a Box, Rc, or &) to break the cycle。这正是 E0073 所描述的“用指针破递归”思路在今日实现中的延续与泛化。

四、标准修复:用 Option 显式提供递归终点

E0073.md 给出的修复方案是引入Option,把“必须有值”改成“可以没有值”:

struct Foo { x: Option<Box<Foo>> }

修复之后,递归链立刻有了合法的起点——直接构造一个空链即可:

let first = Foo { x: None };

这里None就是整个递归的基例:它不需要任何已有Foo参与,于是“第一个实例”得以诞生;后续每个Foo都可以挂在某个已有实例的Some(Box::new(...))上。可见Option的本质作用不是消除递归,而是在递归类型中挖出一个“不引用自身”的终止分支。

在此基础上可以自然延伸出两种同源套路,用于真实的数据结构设计:

  1. 字段可空终止(文档写法):适合“链节点”语义,每个节点记录Some(next)或None(链尾)。
  2. 枚举式基例:把递归结构写成枚举,用独立变体充当叶子。例如把链表建模为:
enum List { Nil, // 基例:不引用 List 自身 Cons { value: u8, next: Box<List> }, // 递归分支:通过 Box 间接引用 }

其中Nil承担与None完全相同的“终止”职责,同时让空与非空两种情况在类型层面显式可区分,代码可读性与模式匹配体验往往更优。

此外,无论采用哪种写法,递归分支都必须保持“引用在Box/&/Rc等间接层之后”,这是保证类型尺寸可计算的前提(对应现行 E0072 的要求);而基例(Option或空变体)则保证类型在值层面“可落地”(对应历史 E0073 的要求)。两者齐备,递归数据结构才既可布局又可构造。

五、今日视角:现代编译器如何处理这类代码

由于 E0073 已停用,现在直接书写struct Foo { x: Box<Foo> }已不会再收到 E0073 报错。从仓库现状推断,编译器的递归类型诊断已经演化为“以尺寸/布局检查为核心”的体系:

  • 递归缺少指针间接层→ 尺寸方程不收敛 → 触发 E0072(现行文档见 E0072.md,并在 E0320.md 中作为“无限大小类型”被引用);
  • 递归已有间接层但无基例→ 类型本身合法,但任何实例化尝试都会要求你写出一个无限长的构造表达式,最终由类型推导或递归深度限制兜底,不再由专门的 E0073 负责。

从实现证据看,rustc_query_impl/src/handle_cycle_error.rs 中以 E0072 报错的循环检测逻辑(recursive without indirection标签 +Box<…>包装建议,见第 258~296 行)已成为递归类型问题的主要诊断出口。E0073 的历史职责——提示“递归需要一个可构造的初始实例”——在实践中转化为编写者应当自觉遵守的设计约束,即任何递归类型都必须带显式终止分支。

六、给使用者的三条实操建议

综合该文档与仓库现状,在面对“结构体里想装一个同类型的自己”这类需求时,可以按以下顺序自查:

  1. 先问:递归字段直接内嵌了吗?若字段类型在展开后会再次原样包含自身(如struct Foo { x: Foo }或缺少Box的Option<List>),类型尺寸不可计算,将触发现行 E0072。必须用Box<T>、&'a T、Rc<T>等间接层包裹,让字段尺寸固定为指针大小。

  2. 再问:间接层之后,递归链有基例吗?若字段是Box<Foo>且没有可终止分支(对应历史 E0073 场景),虽然能通过编译期的尺寸检查,但该类型无法构造任何实例。应把字段改为Option<Box<Foo>>,或改用带Nil这类空变体的枚举,保证能写出Foo { x: None }这样的“首个实例”。

  3. 最后问:能用非递归方案替代吗?若数据天然共享且成环(如图、双向链表),纯Box递归并不合适,通常需要Rc/Weak或基于索引的平坦存储(如Vec<u8>+ 下标)。递归类型应只用于结构上确实呈“树/链”形态的数据。

结语

E0073 虽然已从 rustc 的诊断体系中退役,但它的核心洞察——递归类型不仅要有确定尺寸,还必须存在可构造的“第一个实例”——在今天依然是指引递归数据结构设计的关键原则。理解它与现行 E0072 的分工,等于同时掌握了两条防线:用Box/&/Rc打破尺寸递归,用Option/枚举空变体打破值递归。二者组合,才能写出既可通过编译器检查、又真正可实例化使用的递归类型。

如需进一步查阅,推荐顺序为:先读本文所依据的历史文档 E0073.md,再对照现行文档 E0072.md,最后阅读错误码注册与维护约定 lib.rs 中关于“已停用错误码保留编号并加注说明”的规则,即可完整理解 rustc 错误码体系“既保留编号、又如实标注状态”的文档治理方式。

【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询