- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
本文以仓库中的 text/2056-allow-trivial-where-clause-constraints.md 为骨架,结合仓库内其他 RFC(
where子句语法、一致性规则、宏 1.1 等)与流程文档,系统梳理"平凡约束"问题的来龙去脉、设计动机、具体设计及遗留问题,帮助你理解宏生成代码为何需要这类约束,以及编译器如何通过 lint 而非硬错误来承接旧版 E0193 的语义。
一、RFC 概览:一个问题、两条主线
本 RFC 提出:允许在 where 子句中书写那些"平凡地可知恒成立或恒不成立"的约束。例如:
impl Foo for Bar where i32: Iterator将成为合法代码——这个 impl 永远不会被任何类型满足,但它可以合法地存在。实现该特性的具体手段是移除(或者说废弃)编译器错误 E0193,并新增一个默认 warn 级别的 linttrivial_constraints,用来承接 E0193 在 Rust 1.7 之前的语义。
整篇 RFC 有两条主线:
- 语言层面:放宽 where 子句的约束检查,允许"平凡约束"出现;
- 工程层面:把原本的硬错误降级为默认 warn 的 lint,从而在不破坏宏生态的前提下保留对"无意义代码"的提醒能力。
该 RFC 于 2017-07-05 提出,Feature Name 为allow_trivial_constraints,其 Rust 跟踪 issue 为 rust-lang/rust#48214(该编号同时出现在 text/3399-cfg-attribute-in-where.md 中,被直接称为 "trivial bounds" 议题)。
二、背景知识:where 子句与"平凡约束"从何而来
要理解本 RFC,必须先回到where子句本身的设计。仓库中的 text/0135-where.md 是引入where子句的原始 RFC,其中明确了它的语法(BNF):
WHERE = 'where' BOUND { ',' BOUND } [,] BOUND = TYPE ':' TRAIT { '+' TRAIT } [+] TRAIT = Id [ '<' [ TYPE { ',' TYPE } [,] ] '>' ] TYPE = ... (same type grammar as today)关键点是:where子句可以约束的不只是类型参数,而是任意类型。例如它可以写Option<T>: MyTrait或(int, T): Add<T>,这正是它比内联 bounds 更富表达力的原因(详见 text/0135-where.md 的 "Bounds are insufficiently expressive" 一节)。
但 0135 同时做了一个保守决策:当时规定,"self 类型不引用任何类型参数"的平凡 where 子句(trivial where clauses)是错误。原文给出的例子是:
fn foo() where int : Eq { ... }理由很直接:这类子句没有特别含义——被调用方已经知道所有涉及的类型,约束恒成立或恒不成立,写在这里纯属噪音。0135 明确写道:"这是一个保守选择:如果我们发现确实需要为它们定义特定解释,我们可以在以后让它们合法。"
本 RFC(2056)正是那个"以后":它主张让这类约束合法化,以满足宏(尤其是派生宏)生成代码时的现实需求。
三、动机:为什么有人会想写"必然成立/必然不成立"的约束
乍看之下,把恒成立或恒不成立的约束写进 where 子句很奇怪。RFC 明确指出,需求主要来自宏:
- 自定义派生(custom derive):一个派生宏可能希望在"两个 derive 同时使用"时附加额外功能;
- 代码生成时无法预判约束真值:生成代码的当下,宏作者不一定能轻易判断某个约束是否成立。
RFC 给出了 Diesel 的经典案例。Diesel 希望允许用户用普通 Rust 运算符(如+)来生成等价的 SQL 表达式。由于一致性(coherence)规则的限制,Diesel 无法直接提供一个 blanket impl,但又希望当列的类型支持+运算符时,自动为列实现std::ops::Add。生成的 impl 长这样(RFC 原文):
impl<T> std::ops::Add<T> for my_column where my_column::SqlType: diesel::types::ops::Add, T: AsExpression<<my_column::SqlType as diesel::types::ops::Add>::Rhs>, { // ... }手写代码时没人会写这种 impl——my_column::SqlType的类型我们总是知道的,完全可以手工判断约束是否成立。但站在宏的角度,代码生成时我们并不总能轻易判断这个约束到底成立与否,于是"让编译器接受平凡约束"就成了必要能力。
Diesel 的类型系统在仓库其他 RFC 中也有印证:text/2397-do-not-recommend.md 详细讨论了 Diesel 的AsExpressiontrait 及其SqlType类型参数设计,并展示了当类型不匹配时编译器报出 E0277(trait bound 不满足)的冗长错误链。这说明AsExpression/SqlType这套机制确实真实存在于 Diesel 生态,并深刻影响着 Rust 类型系统的使用方式。
一致性规则为何挡路
"为什么不能直接写 blanket impl"?因为 Rust 的一致性规则(coherence rules)与孤儿规则(orphan rules)严格限制了 impl 的写法。仓库中的 text/1023-rebalancing-coherence.md 对此有系统论述:一个 crate 无法为Type: Trait定义 impl,除非Type或Trait是本地类型;同理,除非Type或Trait是本地类型,否则也不能依赖Type: !Trait成立。而 blanket impl 一旦发布,后续几乎无法在不破坏下游的情况下再添加(见该 RFC 的 "Effect on parent crates" 一节)。
text/0048-traits.md 也从 trait 设计角度讨论了 blanket impl 的利弊。这些规则共同决定了:Diesel 这类库无法用单一 blanket impl 覆盖"所有支持 + 的列类型",只能退而求其次,在宏生成代码时动态写出带平凡约束的 impl——这正是本 RFC 要解锁的场景。
宏观背景上,text/1681-macros-1.1.md(Rust 宏 1.1 RFC)明确指出 serde、Diesel 这类大型生态项目当时几乎都依赖 nightly 编译器,核心原因就是过程宏;该 RFC 旨在把自定义 derive 带入 stable Rust。本 RFC 可以视为这条"宏生态稳定化"路线上的一个配套语言改动。
四、详细设计:移除 E0193,新增trivial_constraintslint
4.1 当前行为(截至 RFC 写作时的 Rust 1.7+)
RFC 指出一个有趣的事实:作为 Rust 1.7 的现状,E0193 实际上从未真正出现过。当前的行为是:
impl Foo for Bar where i32: Copy(约束恒成立的场景)可以正常编译;impl Foo for Bar where i32: Iterator(约束恒不成立的场景)编译失败,但报错信息是"i32未实现Iterator"(E0277 风格),而非专门禁止此场景的 E0193。
也就是说,旧版"显式禁止平凡约束"的错误信息已经名存实亡,本次改动本质上是对现状的规范化:让"恒不成立"的平凡约束也合法化。
4.2 投影类型(projection)的连锁问题
实现本特性时最明显的复杂点在于:放宽约束会连带允许无意义的投影(projection)出现在 where 子句中。RFC 举例如下:
当i32: IntoIterator出现在 where 子句中时,我们也必须允许i32::Item: SomeTrait出现在同一个子句中——因为i32::Item只有在i32: IntoIterator成立时才是有意义的类型投影。更进一步,我们甚至需要允许在 item 体内写for _ in 1,并让这一切全部成功编译。
这打破了"类型系统层面阻止无意义代码"的防线,是设计上必须接受的副作用。
4.3 lint 而非硬错误:保留诊断价值
被 E0193 这类错误捕获的代码,在宏之外通常是废话代码。RFC 认为继续以 lint 形式保留这条检查很有价值:
- 新增 lint
trivial_constraints; - 语义对齐 Rust 1.7 之前的 E0193;
- 默认级别为 warn(而不是 deny/error)。
这样既满足了宏生成代码的需求(不再被硬错误卡死),又让普通手写代码中的"无意义约束"依然能被编译器提醒。
4.4 缺点与未决问题
RFC 承认的主要缺点是:对编译器的改动可能显著增加复杂度(放宽类型检查与 lint 逻辑需要协调)。
Alternatives 一节为 n/a(未提出替代方案)。
唯一的未决问题:trivial_constraints应该默认 warn 还是默认 error?RFC 正文倾向 warn,但把最终决定留给了后续讨论。
五、如何教导用户
RFC 认为该特性不需要专门的教学投入:只要用户了解 where 子句的基本规则,就会自然期待这种写法能工作。换句话说,这是一项"符合直觉、消除例外"的改动,而不是引入新概念。
六、仓库内的关联资料与后续演进线索
在本仓库中,与本 RFC 主题相关的材料包括:
| 主题 | 仓库路径 | 关联点 |
|---|---|---|
| where 子句语法与语义 | text/0135-where.md | 原始设计、BNF 文法、"平凡 where 子句视为错误"的保守决策 |
| 一致性/孤儿规则 | text/1023-rebalancing-coherence.md | 为什么不能轻易写 blanket impl |
| blanket impl 与 trait 设计 | text/0048-traits.md | trait 与 impl 的基础约束 |
| 宏 1.1 与生态现状 | text/1681-macros-1.1.md | serde/Diesel 对过程宏的依赖,本 RFC 的宏观背景 |
| Diesel 类型系统细节 | text/2397-do-not-recommend.md | AsExpression/SqlType机制与 E0277 错误形态 |
| 后续演进(cfg 边界) | text/3399-cfg-attribute-in-where.md | 将 "trivial bounds"(即 issue 48214)列为条件约束的 future possibility |
| RFC 写作规范 | text/0000-template.md | 本 RFC 遵循的仓库标准模板(Summary/Motivation/Detailed design 等章节结构) |
| RFC 流程总览 | README.md | RFC 从提案到合入的完整生命周期 |
值得注意的后续线索:text/3399-cfg-attribute-in-where.md 在 "Future possibilities" 一节明确提到,条件约束(conditional bounds)也可用于解决 trivial bounds 问题(即本 RFC 对应的 issue 48214),且认为无需额外支持——因为条件编译发生在语法层面而非类型层面。这说明平凡约束问题与 cfg 条件约束存在设计上的潜在协同关系。
另外,text/2834-cargo-report-future-incompat.md 中出现了 "trivial constraint" 的用法(描述 nightly 渠道 Cargo 已能本地缓存上游路径依赖构建时产生的警告),可作为"平凡约束"这一术语在仓库语境下的另一处旁证——虽然它讨论的是 Cargo 行为而非本 RFC 的语言特性。
七、总结
RFC 2056 是一个典型的"小改动、深背景"的语言提案:
- 问题本质:where 子句允许约束任意类型,但"与类型参数无关的平凡约束"长期被保守地禁止;宏生成代码却恰恰需要这种写法。
- 解决方案:让恒成立/恒不成立的平凡约束合法化(移除 E0193 的禁止语义),同时用默认 warn 的
trivial_constraintslint 承接旧版诊断价值,兼顾"宏可用"与"手写代码仍被提醒"两个目标。 - 代价与悬念:需要容忍投影类型与无意义代码进入合法范畴,编译器实现复杂度上升;lint 默认级别(warn vs error)留给后续决策。
对于宏作者与类型系统研究者而言,这份 RFC 的价值在于揭示了一个反直觉的设计权衡:类型系统并不是越严格越好——当它严格到阻碍代码生成工具时,把硬错误降级为 lint、把决定权交给用户,往往是更务实的路径。
- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
相关推荐
Rust where 子句详解:RFC 135 如何重塑泛型约束、算子多分派与关联类型设计
Rust where 子句详解:RFC 135 如何重塑泛型约束、算子多分派与关联类型设计 导读 where 子句是今天每一位 Rust 开发者每天都在使用的语
文档Rust 编译错误 E0646 深度解析:`main` 函数为何不允许携带 `where` 子句
Rust 编译错误 E0646 深度解析: main 函数为何不允许携带 where 子句 导读 本文围绕 Rust 编译器(rustc)错误码 E0646 展
编程语言编译器语言运行时标准库Rust where 子句完全指南:在 rust-by-example 中掌握泛型约束的声明式写法
Rust where 子句完全指南:在 rust by example 中掌握泛型约束的声明式写法 导读 本指南以 rust by example 仓库中的 s
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考