资讯详情

Rust “一致性”和“孤儿规则”深度解读

📅 2026/10/30 2:45:39 | 华诺云谱 👁 阅读
Rust “一致性”和“孤儿规则”深度解读
文章目录1. 什么是一致性2. 孤儿规则杜绝多重实现维持一致性3. Fundamental Types孤儿规则排除在外的类型4. Covered Implementations孤儿规则特许的为外部 type 实现外部 trait 的一类 impl5. 重新认识孤儿规则5.1 impl 的声明形式5.2 简单示例5.3 完备的“孤儿规则”6. 孤儿规则的推导6.1 目标和现状6.2 第 1 条规则为什么本地 trait 不用检查直接允许6.3 第 2 条规则如果是外部 trait为什么必须要“锚定”一个本地类型?6.4 第 3 条规则解决“对称冲突”7. Blanket Implementations孤儿规则理论衍生的 impl 分类8. 重新梳理知识结构引言Rust 的“一致性”Coherence是要解决一种 “type trait” 组合只允许有一种 impl 这样一个问题“孤儿规则”Orphan Rule是构建这种“一致性”Coherence的规则框架。大部分情况下人们只使用简单的孤儿规则即可判断出一种 impl 是否合法但是在一些较为特殊的场景下为了能为外部的 trait 和类型添加 implRust 引入了一个狭窄的豁免条款RFC 2451这是真正完备的“孤儿规则”但复杂得有点可怕。“孤儿规则”是可以推导出来的经历一遍推导过程可以让我们彻底理解它本文我们希望把大家带到“河的对岸”。正文一致性Coherence和孤儿规则Orphan Rule在 Rust 中的影响面非常之广但大多数情况下我们根本感觉不到它们的存在当真正遇到问题的时候我们才会发现它们是如此复杂和烧脑。比如下面这个问题为什么在implT FromLocalTypeT for VecT中From和Vec都不在当前 crate 中但这种 impl 却是合法的?我们并不想拿一些奇特、生僻的示例进行某种炫技式的表演以上 impl 并不算复杂在平时的工作中是有可能会写出来的更重要的是这个案例其实对应的是“孤儿规则”中最深的一个逻辑分支是我们本文将会探索到的最深的地方。此外还有一个并不是那么重要但想搞清楚也并不容易的问题为什么implT LocalTraitT for VecT会被视作 blanket impl而implT LocalTraitVecT for VecT则不是这看似是一个关于 blanket impl 的问题但解答它需要借用“孤儿规则”建立的理论工具所以它也是我们本文要探讨的支线话题之一。当然本文的主旨是全面、深入地介绍“一致性”和“孤儿规则”并不是针对以上两个问题展开的它们的答案会在理解了本文的全部内容后不言自明。备注本文以 Rust for Rustaceans 一书中的 Coherence and the Orphan Rule 一节的内容为蓝本参考 RFC 2451 的官方解释追加了大量的解读和示例说明要比两个原始出处的介绍理解起来容易得多但即便如此本文自第 5、6 两节的内容依然是很艰深的需要耐心地花一些时间才能完全吃透。另外推荐本公众号近期的一篇精华好文《Zed / GPUI 设计觉醒现代编辑器 Action 机制解读》1. 什么是一致性在 Rust 语言中有typestruct, enum, union和trait我们经常需要为一个 type 去实现一个 trait也就是编写一个impl 语句块构建出一个集属性和行为于一体的实用类型所以在 Rust 中我们构建和使用的其实一直是“type trait” 的组合。由于 type、trait、impl 语句块都是独立的语法结构可以分散在不同的地方编写于是不同的人可能会在不同的地方各自实现了同一种 “type trait” 组合。也许这在单一项目中不会发生但如果 type 和 trait 分别来自于两个不同的被广泛使用的 crate 的话那么它们的组合实现将会爆炸式增长每一个需要使用它们的第三方库和项目都需要编写自己的实现。而如果再把泛型引入进来的话就会在另一种维度上增加这种“重复实现”的可能性。针对同一种 “type trait” 组合的多种实现势必会带来混乱编译器无法确定应该采用哪一种实现于是Rust 规定针对一种 “type trait” 组合只允许有一种 impl这就是所谓的“一致性”Coherence。2. 孤儿规则杜绝多重实现维持一致性规定有了要采取什么措施才能杜绝“多重实现”呢我们假设 type 和 trait 分属两个 crate这是常态则方法一impl 完全倒向 trait 那一侧只允许在 trait 所在的库中为一个外部的 type 编写它的实现代码也就是限制 impl 语句块只能和它的 trait 待在同一个 crate 中如此一来只有 trait 所在的库才能实现 trait别的库实现不了这样就不会有重复实现了。但这一决策的副作用也是非常大的如果大量的第三方库也想为自己的类型实现这个 trait它们不能在自己的 crate 中编写只能在 trait 所在的这个库中为各种第三方类型编写这个 trait 的实现也就是说这个海量的工作会落到 trait 库的维护者身上这是不切实际的工作量大且无法穷尽。方法二impl 完全倒向 type 这一侧只允许在 type 所在的库中为一个外部的 trait 编写它的实现代码也就是限制 impl 语句块只能和它的 type 待在同一个 crate 中这虽然解决了方法一面临的问题却带来了方法一没有的问题那就是如果想为大量的第三方库中的类型实现这个 trait不能在 trait 所在的 crate 中编写只能到各个第三方库中去为它们的类型编写这个 trait 的实现也就是说这个海量的工作会分散到各个第三方库的维护者身上这同样是不切实际的根本没有这种协作模式。以上两种“单边倒”的策略都会“顾此失彼”理想的处理方式是找到一种均衡的策略避免以上两种方法中出现的极端状况。这种平衡策略就是既允许 impl 在 trait 所在的 crate 中写也允许 impl 在 type 所在的 crate 中写但前提是 impl 针对的 trait 或 type 必须有一个是定义在当前 crate 中的也就是说只有当 trait 或 type 二者有其一是定义在你的 crate 中时你才能在自己的 crate 中为这个 type 实现这个 trait这就是“孤儿规则”Orphan Rule。有了“孤儿规则”之后你就能为一个外部定义在其他 crate 中的 type 实现自己的 trait同时也可以为你自己的 type 添加外部 trait 的实现。典型的案例就是你既可以为自己的类型实现标准库中的Displaytrait又可以为VecT类型实现你自己的Paginatetrait一个由你自定义的分页功能但你却不能为VecT类型实现Display因为这里的 type 和 trait 都不属于你的 crate孤儿规则会禁止这种行为。然而需要提前告诉大家的是以上关于孤儿规则的解释其实并不完全准确它只是一个易于理解的简化版本因为你没法用它去解释我们在文章开头提到的那种情形为什么在implT FromLocalTypeT for VecT中From和Vec都不在当前 crate 中但这种 impl 却是合法的要解释这个问题必须使用完备版的“孤儿规则”这要等到 5.3 节才能展开。3. Fundamental Types孤儿规则排除在外的类型有些类型极为重要因此必须允许任何人为其实现各种特性即便这似乎违反了孤儿规则。这些类型会被标记上#[fundamental]属性例如、mut以及Box。就孤儿规则而言这类基本类型几乎可以被视为“不存在”或者说“透明”孤儿规则判断一个 type 或 trait 归谁时如果遇到、mut、Box这些类型会直接跳过这一层往里看。比如BoxLocal会被视为Local从而被判定为本地类型但VecLocal则不可以。这样规定的原因是像、mut以及Box这些“修饰”和“包裹”太过普遍而人们需要的是为它们“修饰”和“包裹”的类型实现 trait所以在判定一个类型是本地还是外部时并不考虑类型中的、mut、Box这些类型的修饰和包裹。这样我们就可以为MyType实现IntoIterator这样的特性了。如果仅依据“孤儿规则”这种实现方式是不被允许的因为它是为一种非标准类型的特性而实现的毕竟IntoIterator和都来自标准库。Fundamental Type 可以被视作是“孤儿规则”为一些极为常用的类型开的一个“后门”。4. Covered Implementations孤儿规则特许的为外部 type 实现外部 trait 的一类 impl孤儿规则禁止为外部 type 实现外部 trait这个规则其实是非常严格的特别是当 type 和 trait 都来自于重度使用的库比如标准库时于是“孤儿规则”在 Fundamental Type 之外开了另外一个“后门”。考虑这样一种典型的场景在我们当前自己的 crate 也称“本地”中有一个名为PairT的结构体它有a、b两个字段我们很希望能在它和VecT之间建立 from/into 的转换关系就是这样letpPair{a:10,b:20};letvVec::from(p);这需要我们为VecT实现FromT这个 impl 语句块应该是这样的implTFromPairTforVecT{fnfrom(pair:PairT)-Self{vec![pair.a,pair.b]}}对于Pair所在的这个 crate 来说VecT和FromT都是外部的按照前面介绍的孤儿规则这是不允许的然而实际上它却是可行的上面这种 impl 形式就是“Covered Implementation”注意这个名称并不是 Rust 的官方术语而是 Rust for Rustaceans 一书的作者起的因为官方没有针对这种 impl 起名但这个名称起得非常好covered 一词抓住了这个 impl 的关键特征阅读完后面的章节你会体会到这一点因为在这里孤儿规则引入了一个狭窄的豁免条款在 RFC 2451 中正式提出允许在非常特定的情况下为外部类型实现外部 trait。如果我们能找到一种约束规则允许为外部 type 实现外部 trait 而不会引发重复的 impl这当然是一件天大的好事但问题就在于绝大多数情况下为外部 type 实现外部 trait 的 impl 根本找不到这种规则而 Covered Implementations 是目前唯一能“圈选”出所有可能的重复实现并“确定”其中一种实现的规则所以它才能被引入进来关于对 Covered Implementations 更深刻的解读要等我们重新认识孤儿规则之后才会对它有更深刻的认识。5. 重新认识孤儿规则我们在第 2 节介绍了孤儿规则在第 3、4 节介绍了孤儿规则的两种例外情形特别是对于 Covered Implementations我们不得不问的是为什么implT FromPairT for VecT这种 impl 能被允许还有别的类似形式吗这里总该有个“标准”吧当然得有而且这个“标准”还是“孤儿规则”定的准确地说是完备的“孤儿规则”而我们在第 2 节介绍的孤儿规则其实是一个简化版本。在这一节我们来重新认识一下孤儿规则。5.1 impl 的声明形式首先为了让解释更加准确和清晰我们必须对 impl 声明中出现的各种类型进行一下编号因为这些编号以及它们的顺序也会在规则中提到。我们用下面这种形式来表示注意这是 Rust 语言参考手册中使用的官方描述形式implP1..PnTraitT1..TnforT0^^^^^^^^^^^^^^^^^^^^^^^泛型参数|trait|类型T1..Tn|类型T0..是借用了 Rust 的闭区间语法1..n表示1,2, …,n。所以P1..Pn表示P1,P2, …,Pn即一组泛型参数T1..Tn表示T1,T2, …,Tn即一组类型T0就是 Self 的类型它被当作“第 0 个类型”要和 trait 的类型参数一起排序在后面的检查规则中会使用这个编号。注意T0是 for 后面的整个部分如果是for VecT那T0 VecT而T1..Tn则是 trait 尖括号里的类型如果是MyTraitT for那T1 T如果是MyTraitVecT for那T1 VecT此外后面还会出现T0..Ti..是左闭右开区间表示T0,T1, …,T(i-1)不包含Ti。以上标记完全展开的话就是下面的样子implP1,P2,…,PnTraitT1,T2,…,TnforT0^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^泛型参数:P1..Pn|trait|类型T1..Tn|类型T05.2 简单示例为了能更好地理解并熟练认出以上 impl 描述形式中的符号和编号我们需要做几个小练习从易到难慢慢递进。例 1impl 没有泛型参数trait 也没有类型实参implDisplayforMeters记号内容P没有T0MetersT1…Tn没有Display不带类型实参例 2impl 没有泛型参数trait 有一个类型实参implFromi32forMeters记号内容P没有T0MetersT1i32例 3impl 有多个泛型参数trait 也有多个类型实参假设有一个带两个类型参数的 traittrait ConvertA, B {}。implX,YConvertX,VecYforOptionX记号内容其中的 P 是否 coveredP1X类型参数声明不适用P2Y类型参数声明不适用T0OptionXX被Option包着所以X是covered typeT1XX单独出现没被任何类型包裹所以X是uncovered typeT2VecYY被Vec包着所以Y是covered type在这个示例中我们第一次看到 covered type 和 uncovered type 这两个概念这是 Rust 语言参考手册中定义的重要概念简单介绍一下covered type作为另外一种泛型类型的类型参数出现的类型。covered 意为有一层覆盖物的以示例中出现在T0位置上的OptionX为例类型X就是被类型Option覆盖着的所以T0位置上的X是 covered注意指的是X不是包裹它的外层类型uncovered type就是单独出现没有作为另外一种泛型类型的类型参数出现的类型以示例中出现在T1位置上的X为例类型X单独出现没有被任何类型覆盖所以T1位置上的X是 uncovered5.3 完备的“孤儿规则”OK, 所有的基础概念都已介绍完毕我们重新看一下真正的“孤儿规则”描述吧trait 是本地的吗即trait 是在当前 crate 中定义的吗若是直接 ☑ 允许就是前面给出的简化版的孤儿规则否则从 T0 开始往后找找到第一个本地类型记为 Ti。若一个都没找到直接 ☒ 禁止若找到检查 Ti 前面的那些 T不含 Ti有没有 uncovered 的 P。没有则 ☑ 允许有则 ☒ 禁止其中Ti也就是第一个本地类型在孤儿规则的判定中非常重要后面会反复提到它所以得给它起一个名字本文我们把它叫“锚点”至于为什么会这么叫后面会再解释。以上判定规则用下图会表示得更清晰一些让我们拿几个例子练习一下From是标准库中的 trait显然是外部的Local是一个本地类型下面的表格列举了几种 impl 形式然后给出了判定它们是否合法的过程本地书写的 impl第 1 步第 2 步第一个本地类型 Ti第 3 步Ti 之前有没有 uncovered P结果impl FromLocal for VecLocalFrom不是本地 trait继续往下看T0 VecLocal不是本地类型T1 Local是所以 i 1只需检查 T0T0 里没有任何 P☑允许implT FromLocal for VecTFrom不是本地 trait继续往下看T1 Locali 1T0 VecTT是 covered☑允许通过 RFC 2451 新放开的写法implT FromLocal for TFrom不是本地 trait继续往下看T1 Locali 1T0 T是 uncovered☒ 禁止implT FromT for VecTFrom不是本地 trait继续往下看T0 VecT不是本地类型T1 T也不是第 2 步已经失败☒ 禁止这个完备版本的“孤儿规则”能完全兼容前面所说的只有当 trait 或 type 二者有其一是定义在你的 crate 中时你才能在自己的 crate 中为其实现该 trait同样也能解释第 5 节介绍的 Covered Implementations同时还能杜绝后面会提及的一种被称为“对称冲突”的漏洞所以以上的描述是最完备的。最后我们用上面介绍的新规则来推导一下它是否真的兼容“简化版”的孤儿规则当 type 是本地类型时implP1, P2, …, Pn ForeignTraitT1, T2, …, Tn for LocalType第 1 步ForeignTrait 不是本地 trait继续往下看第 2 步第一个本地类型是 T0 LocalTypei 0第 3 步Ti 之前(T0 之前)已无任何 T 类型自然也不会有 uncovered 的 P结论合法当 trait 是本地 trait 时implP1, P2, …, Pn LocalTraitT1, T2, …, Tn for ForeignType第 1 步LocalTrait 是本地 trait直接通过检查结论合法6. 孤儿规则的推导完备版本的“孤儿规则”非常强大如同公式一样准确但这正是我们困惑的地方为什么为什么是这样“无厘头”的规则这背后肯定有某种内在的逻辑所以我们还得再往下深挖一层我们来试着推导一下孤儿规则。6.1 目标和现状目标任意一个具体的 “type trait” 组合全世界最多只有一个 impl。现状crate 之间的依赖关系构成一个“有向无环图”一个 crate 只能看到自己和自己的上游依赖两个 crate 之间只可能是“上下游关系”或“互不依赖关系”中的一种。“上下游关系”就是依赖关系“上游”指的是你的 crate 所依赖的 crate而“下游”则是指依赖你这个 crate 的那些 crate“互不依赖关系”则是指两个 crate 彼此没有出现在对方的上游中也可称之为“平行关系”。注意crate 之间的关系其实就是一个单纯的“有向无环图”问题不管是“上下游关系”还是“互不依赖关系”它们都不是限定在某个项目中的关系而是在所有 crate 构成的全局 DAG 中crate A 是否依赖 crate B由 A 自己的 Cargo.toml 决定和 A 被用在哪个项目里无关。所以“A、B 之间是‘上下游关系’还是‘互不依赖关系’在所有项目中都一样”。上述两种关系在 DAG 中其实是有专门术语的分别叫可比较comparable和不可比incomparable只不过太抽象了所以我们还是用“上下游关系”和“互不依赖关系”来描述。与此同时在一个项目中项目本身是一个 crate它会依赖多种 crate这些 crate 之间也有两种关系“父子关系”和**“兄弟关系”**它们和“上下游关系”与“互不依赖关系”是两套不同的关系。比如原本是“互不依赖关系”的两个 crate它们在项目中可以通过Cargo.toml安排成“兄弟关系”原本是“父子关系”的两个 crate 也可以通过Cargo.toml安排成“兄弟关系”但这些“安排”改变不了两个 crate 之间本来的关系因为这些依赖都是从项目所对应的这个 crate 到这两个 crate 建立的 DAG 中的“边”而不是两个 crate 之间的“边”。你可能会认为项目所对应的这个 crate 不就是在这两个 crate 中间建立的新“边”吗不是因为边是有方向的看一下图就知道了假设 A 和 B 是两个互不依赖的 crate在 app 中它们被定义成了“兄弟”这并不表示 A 和 B 通过 app 建立了“间接”的关联因为依赖也就是边是有方向的从 A 或 B 是无法走到对方的因为没有 A - app 或 B - app 这样的“边”app / \ ↓ ↓ A B上面这个示例正好能揭示产生冲突的 impl 的原因我们假设 A 和 B 都写了覆盖同一种 Covered Implementations由于它们是“互不依赖”关系彼此看不到对方所以它们各自独立编译和使用时不会出现冲突但当 app第三个 crate同时引入它们时冲突就暴露出来了编译能看到两个 impl 吗结果A只能看到自己的通过B只能看到自己的通过app两个都能看到冲突在这里才暴露但 app 改不了 A 和 B 的代码孤儿规则正是为了解决这一问题才提出的。我们说基于 crate 间的这两种拓扑关系假设有 A、B 两个 crate在两种情况下两个 crate 可能会写出冲突的 impl情形拓扑关系例子冲突能否被编译器发现1上下游关系A 在 B 的上游依赖中或 B 在 A 的上游依赖中能。B 编译时能看到 A 的 impl有冲突编译器会报错2互不依赖关系A、B 在各自的上游依赖里看不到对方不能。各自编译都通过直到第三个 crate 同时依赖 A 和 B 时才会发现冲突孤儿规则的提出就是要杜绝第 2 种情形让两个互不依赖的 crate 写不出相互冲突的 impl因为第 1 种情形在编译时可以检查出来而第 2 种情形编译器是检查不来的只能通过一些“规则”事先规避这些规则当然也是编译器的规则但跟编译器检查上下游关系中的 impl 冲突完全不同这些规则是“事前干预”是通过检查 impl 的形式在发生冲突前就报错规避掉了而检查上下游关系中的 impl 冲突则是“事后干预”是发现了具体的 impl 冲突后才报的错。那这个”规则“要怎么定呢一定是要围绕目的去设计我们继续往下分析。6.2 第 1 条规则为什么本地 trait 不用检查直接允许在第 1 条规则中若 trait 是本地的则直接判定合法为什么呢因为一个 trait 只会定义在一个 crate 里任何别的 crate 想给这个 trait 写 impl必须能“看到”它也就是必须依赖 trait 所在的 crate。所以 trait 所在的 crate 和其他想要为这个 trait 写 impl 的 crate 必定是上下游关系这自然会有编译器负责检查冲突孤儿规则不用管。6.3 第 2 条规则如果是外部 trait为什么必须要“锚定”一个本地类型?在第 2 条规则中我们需要找到第一个本地类型Ti我们管它叫“锚点”为什么需要一个“锚点”呢因为对于没有锚点的 impl 来说如果 Rust 允许它被写出来那人们就可以在无数个 crate 里写出无数种冲突的 impl。像下面这种典型的无锚点 impl如果这种 impl 形式被放开那冲突的 impl 将无法避免:implTForeignTraitTforForeignTypeT如果我们能找到一种合理的规则让针对某种“type trait”组合的 impl 只能写在一个特定的 crate 中那冲突就能化解。一种可能的思路是只要在 impl 的头部声明中有一个本地类型不管是在 for 后面还是在外部 trait 的尖括号中便算是跟当前的 crate “有些关系”借着这层“本地类型”的关系让这种 impl 只能在当前这个 crate 中书写也就是把这个 impl “锚定”到当前的 crate 中。这个想法是靠得住的因为本地类型只有当前 crate 以及它的下游 crate 知道与这个 crate 互不依赖的 crate 看不到这个类型也就写不出它的 impl而下游的 crate 虽然能写出重复的 impl但编译器可以帮助识别出来这样分析的话这个规则几乎已经是完备的了。但坦率地讲我没法正向推导这个设想也就是说我不知道“锚定一个本地类型“的点子是怎么被想来的尽管可以借助 AI但很快我就意识到这是一个极为复杂的命题里面有多种变量且这些变量也不都是自变量部分变量之间还有耦合关系这让这个问题异常难解所以我没能完成这项工作。这里我们只能给出一个关键性的结论锚点规则生效之后能覆盖一种“type trait”组合的 impl 被限制到了一类 crate 上这些 crate 在一条上下游链中如果出现冲突的 impl编译器可以发现。6.4 第 3 条规则解决“对称冲突”不过这一逻辑有一个漏洞假设ForeignTrait是定义在第三方 crate F 中的一个 traitA 和 B 是互不依赖彼此不可见的两个 crate在它们各自的 crate 中可能出现下面两种 impl/// 在 crate A 中implTF::ForeignTraitA::LocalforT{}// 包含本地类型 A::Local/// 在 crate B 中implTF::ForeignTraitTforB::Local{}// 包含本地类型 B::Local两个 impl 都包含各自的本地类型但当 A 中的T B::Local、B 中的T A::Local时两者就会覆盖同一种“type trait” 组合(B::Local,F::ForeignTraitA::Local)两个互不依赖的 crate 出现了冲突原因在于A 自己的锚点在 T1但 A 在 T0 留了一个 uncovered 的 PB 可以把自己的锚点填进这个位置B 也同样在 T1 给 A 留了位置A 也可以把自己的锚点填进这个位置。 两边都允许“type trait”组合中的另一个位置可以是“任何人”所以才出现了冲突的实现。我们把这种冲突称为“对称冲突”。“对称冲突”发生在两个互不依赖的 crate 中编译器是检查不出来的只能靠添加规则来修补漏洞这就是孤儿规则中的规则 2、3 要解决的问题孤儿规则的 2、3 条款合在一起表述就是从 T0 往后找每个 crate 以自己的第一个本地类型 Ti 作为锚点并且锚点之前不能有 uncovered 的 P这个看上去很无厘头的规则其实是在“对称冲突”这个场景下作出的一个“二选一”的决定那具体来说这个“二选一”到底是什么样的呢把“对称冲突”这个问题摊开来看之所以会出现对称冲突是因为从 T0 到 Tn我们选中了第一个本地类型 Ti 作为锚点后T 的集合中出现了 uncovered 的 P只要出现 uncovered 的 P就有出现“对称冲突”的可能原因我们在上一节的示例中已经解释过了这里再复述一遍因为A 自己的锚点在 T1但 A 在 T0 留了一个 uncovered 的 PB 可以把自己的锚点填进这个位置B 也同样在 T1 给 A 留了位置A 也可以把自己的锚点填进这个位置。假如我们能找出对称冲突中两种 impl 的不同那我们就可以借助它们的差异选出其中的一种作为合法的 impl然后杜绝另一种对称冲突就可以化解可以说这就是孤儿规则最原始的设计动机那对称冲突中的两种 impl 有不同吗有虽然它们的最终形式是完全一样的但是那个促成它们最终一模一样的“变量”uncovered 的 P 的位置是不一样的在两种冲突的 impl 中这个 uncovered 的 P 会分别出现在本地类型也就是 Ti的前面和后面看下面的例子就清楚了impl锚点uncovered 的 P 位置结果AimplT F::ForeignTraitA::Local for TT1uncovered 的 PT0在锚点之前☒ 禁止BimplT F::ForeignTraitT for B::LocalT0uncovered 的 PT1在锚点之后☑允许孤儿规则中的“锚点之前不能有 uncovered 的 P”实际上就是选择了“uncovered 的 P 出现在锚点之后”的那个 impl 实现禁止了“uncovered 的 P 出现在锚点之前”的那个 impl 实现。实际上如果你是 Rust 语言的主导者如果你有那个权力的话完全可以把规则反过来也就是改为“锚点之后不能有 uncovered 的 P”可以非常确定的是反过来的规则和现在的规则是一样安全的。至于为什么 Rust 选择了“锚点之前不能有 uncovered 的 P”我们没有找到官方的解释在 RFC 1023最早引入“第一个本地类型 Ti”这条规则的 RFC和 RFC 2451 都没有给出解释。但原因是可以推测出来的因为现有的规则更符合人们的设计习惯人们往往习惯性地把 Self 当作 impl 关系中的主要对象把 trait 当作次要对象。因此常见的扩展往往是类型的作者为自己的类型实现外部 trait而现有规则会优先认领 for Self 的 impl 形式这就顺应了人们的设计习惯。7. Blanket Implementations孤儿规则理论衍生的 impl 分类最后让我们来聊聊 Blanket Implementations这好像是个临时插入的话题因为它和我们的主线“孤儿规则”关系不大但有一点交集应该说无论是否有孤儿规则Blanket Implementations 都会存在但是在盘点一个 impl 是否是 Blanket Implementations 时需要使用到“孤儿规则”建立的理论工具。我们先简单介绍一下什么是 Blanket Implementation它指的是这样一种 impl 形式implT:BondMyTraitforT{...}这一形式的一个鲜明特征就是for 后面是一个 uncovered 的 T这意味着它一次性地为多种类型实现了MyTrait这个 trait。其中文名称可以叫“统一实现”我个人觉得翻译成“地毯式实现”会更好借用“地毯式轰炸”一词可以清楚地理解它的含义。blanket impl 只能在 trait 所在的 crate 中实现因为 type 那一侧是泛型不对应某个具体的 crate。如果一个 crate 在引入一个 trait 之初没有为其提供对应的 blanket impl 的话则若在后续的版本中再添加 blanket impl就会被视为破坏性变更breaking change是指原来能正常编译、运行的代码在库升级版本之后编译失败、无法运行的情况因为那些使用了impl MyTrait for Foo的 crate 可能会因为定义该 trait 所在的 crate 更新而导致编译失败出现与现有实现冲突的错误。关于 Blanket Implementations有一个非常 tricky 的问题也就是文章开头提到的那个问题为什么implT LocalTraitT for VecT会被视作 blanket impl而implT LocalTraitVecT for VecT则不是刚接触 Blanket Implementations 概念的读者一般会认为只有像前面示例中的那样出现了一个“裸 T”的时候才能叫“地毯式”实现显然问题中的两种形式都不算。实际上这一理解还是过于“表面”并没有认识到 Blanket Implementations 概念的实质那它的实质是什么呢实际上Blanket Implementations 的判定规则是只要 impl 的头部声明中出现了 uncovered 的 P都是 blanket impl这里完全复用了孤儿规则为 impl 头部声明设置的术语。基于这个判定原则我们来重新分析一下文章开头给出的两个示例implTLocalTraitTforVecT首先trait 是本地的所以这是一个合法的 impl然后T0 VecTT1 TT1也就是 P1是 uncovered所以这是一个 Blanket Implementation。再看下面这个示例implTLocalTraitVecTforVecT同样的trait 是本地的所以这是一个合法的 impl然后T0 VecTT1 VecT)T1 中的 P1 是被Veccovered 的所以这不是一个 Blanket Implementation。8. 重新梳理知识结构在透彻理解了以上所有概念之后我们就可以清晰地看到这些概念之间的逻辑脉络了作为本文的收尾是很好的一个总结。首先“一致性”是总的纲领也是所有规则和概念的最终目的那就是保证一种 “type trait” 组合只能有一种 impl。在这个愿景之下人们最初的设想是“单边倒”策略即要么让 impl 完全定义在 type 所在的 crate要么让 impl 完全定义在 trait 所在的 crate但两种方案都有问题于是Rust 设计了“孤儿规则”在两套方案中做了平衡即既允许 impl 在 trait 所在的 crate 中写也允许 impl 在 type 所在的 crate 中写但前提是 impl 针对的 trait 或 type 必须有一个是定义在当前 crate 中的这基本上确立了“一致性”的整体框架。但是由于孤儿规则要求 impl 针对的 trait 或 type 必须有一个是本地的这就完全禁止了为外部 type 实现外部 trait 的可能但在某些特殊场景下就是 Covered Implementations 所对应的场景人们确实有充足的理由需要这样做于是 Rust 给“孤儿规则”添加了一个狭窄的“豁免条款”即RFC 2451让像 Covered Implementations 那样的实现合法化。而 Fundamental Types 则是孤儿规则针对类型设立的“过滤规则”所有被 Fundamental Types 修饰或包裹的类型在判断类型是本地还是外部时并不会把外层的 Fundamental Types 考虑进去而是直接忽略它们直接考察它们修饰或包裹的类型。Blanket Implementations 则是相对于 Conditional Implementations 的一种 impl 分类。它跟孤儿规则之间并没有太直接的关系但是判定一个 impl 是不是 Blanket Implementation 需要使用到孤儿规则建立的理论工具。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑