超越组件总和的更强类型
摘要
作者分享了一种Rust编程技巧,通过为枚举的每个变体创建独立类型来提高类型安全性,具体针对路径组件,基于std::path::Component。
<p><a href="https://lobste.rs/s/tasulj/type_stronger_than_sum_its_components">评论</a></p>
查看缓存全文
缓存时间: 2026/09/25 17:15
# 一种比各部分之和更强大的类型
来源:https://www.schneems.com/2026/09/24/a-type-stronger-than-the-sum-of-its-components/
你是否曾写过一种让自己爱不释手、至今仍念念不忘的类型?就像品尝过一顿绝佳美食,只要用心回忆,仿佛仍能品尝到舌尖上的滋味。前几天我体验了一次Rust带来的小确幸,想与你分享这份喜悦。
简而言之:我将包含N个变体的枚举拆分为N种独立类型。虽非颠覆性创新,却让我的开发工作大为顺畅。
具体来说,`std::path::Component`(https://doc.rust-lang.org/std/path/enum.Component.html)是一个枚举类型,任何`std::path::Path`(https://doc.rust-lang.org/std/path/struct.Path.html)引用都能返回该枚举。路径可视为组件的迭代器。举例来说,路径`/tmp/hello`对应于`\[Component::RootDir, Component::Normal\("tmp"\), Component::Normal\("hello"\)\]`。这个枚举在路径分解与处理中非常实用,但若接口仅接受某个可能为任意变体的组件,其适用范围就过于宽泛了。
在一个频繁处理路径的库(https://github.com/schneems/path_facts)中,我为每种组件类型创建了独立的所有权结构体,从而能够编写如下函数签名:
``
impl AbsPath {
// ...
pub(crate) fn join_normal(&self, path: &NormalComponent) -> AbsPath {
AbsPath(self.as_ref().join(path.as_ref()))
}
}
``
其中`NormalComponent`是`struct NormalComponent\(OsString\)`,确保源自`Component::Normal`变体。在上例中,我利用这种类型的特性来保证:将它与已绝对路径连接,结果必然仍是绝对路径。
这听起来或许不算惊天动地,但此前我只能这样写:
``
pub(crate) fn join_normal(&self, path: &OsStr) -> AbsPath
``
然而`OsStr`可能包含任何内容——可能是`..`,也可能是绝对路径(此时`join`API会替换目标路径)。另一个用例是用它表示目录中的条目(https://github.com/schneems/path_facts/pull/27/changes/8a1bc039fdd5b1e7186afa38485fb2bf42a5aa54#diff-c34cb87be68214956fa84ca439e1a52f71ace0cae10c5f8e5e15d072aace9ed8R49-R50)。
事后看来,这个思路如此自然:将现有设计精良的枚举拆分为每个变体对应的独立类型。如果知道“拥有N种可能性之一”(即枚举)很有价值,那么确切知道“拥有某个既能融入该枚举、又具备特定属性的实例”也必然有用。枚举也被称为“和类型”。换个角度思考:既然拥有和类型有益,那么拥有构成该和类型的各个独立组件同样有益。
在此之前,我创建了一系列其他类型:
- AbsPath - 经过“绝对化”处理的路径
- RelativePath - 顾名思义,相对路径
- CanonicalPath - 经过“规范化”处理的路径
我发现这些类型虽有用但仍显宽泛。路径处理有个奇特之处:它们既代表词法值,也代表物理位置,而两者可能不一致。词法上的相对路径可能是磁盘上具有绝对目标地址的符号链接。尝试规范化或转换路径可能产生意外结果。比如尝试获取`/path/to/location/skipped/..`的元数据,若`skipped`不存在或非目录则会失败。但若先规范化路径,则能成功获取`/path/to/location`的元数据。
绝对路径未经规范化,因此可能包含`..`(`ParentDir`)和`.`(`CurDir`)。但若不清楚路径的具体用途(在我的库中,我无法知晓用户为何请求特定路径的元数据),就无法安全地对这些值进行规范化,除非已解析它们的物理父路径。这是因为上文提到的`/path/to/location/skipped`也可能是指向完全不同的绝对路径的符号链接,必须在“应用`..`折叠父目录”之前先解析该链接。更复杂的是,Windows存在特殊路径语法会改变查找行为——以`\\\\?\\`开头的路径(如`\\\\?\\C:\\windows`)会将`.`和`..`视为字面值。
这意味着当使用`std::path::canonicalize`处理路径时,返回的是具有该语法的路径。这也导致在Windows系统上执行`canonicalize\(canonicalize\(&path\).join\(&other\)\)`可能不安全。若`&other`包含`..`,将产生字面量查找并很可能失败。值得庆幸的是,`Component`解析在此处保持一致,遇到`..`时总会返回`Component::ParentDir`而非`Component::Normal\("\."\)`。
连接操作的安全性或许是这种新类型带来的最大收益,但能写出这样的代码也很有趣:
``
fn up(position: Reached, name: ParentDirComponent) -> (Step, Reached)
``
这里,`up`函数逐个组件地遍历磁盘路径。此前这里接收`OsString`类型,要求程序员格外小心。现在的类型签名强制开发者必须在调用此逻辑前,向编译器证明手中确实持有`..`组件。这虽非颠覆性创新,但这种严谨的确定性在这里...实在令人愉悦。
正如各人口味不同,人们对类型设计的偏好也各异。若你不喜欢这里呈现的范例也无妨,但我认为这个思路令人满足,希望能为你带来启发。我很期待听到你仍在品味的其他精妙类型模式。
AI免责声明:关于生成式AI编程工具——我也大量手工编写代码,并主张设立类似“手工编码星期一”的实践。这个`src/component\.rs`纯粹是我的肉脑结晶。实际上我是在汽车里编写这段代码的,当时没有网络,正等待孩子们足球训练结束。我使用Grammarly(非生成AI模式)来辅助润色文稿。
相似文章
Rust 中的函数式状态机:Typestate 和 Newtype 模式
本文介绍了如何在 Rust 中使用 typestate 和 newtype 模式实现函数式状态机,以实现代码中的类型安全状态管理。
擦除存在类型
深入探讨 Rust 类型系统中的存在量词,比较 `dyn Trait` 和 `impl Trait`,并探索超越 `Self` 的存在量化类型变量的高级模式。
Rust中基于GADT风格枚举的零成本'Tagless Final'实现
本文探讨了在Rust中使用GADT风格枚举实现'tagless final'模式,展示了编译器如何通过擦除抽象来实现零成本性能。
安全变得简单 第1部分:单一所有权(并非)可选
本文介绍了一种基于线性类型和抽象解释的内存安全新方法,旨在比Rust更符合人机工程学原理地消除诸如释放后使用和内存泄漏等常见错误。
自定义错误在我的Rust应用中是必不可少的
一篇提倡在Rust应用中使用自定义错误类型的博客文章,解释了如何使用map_err和From trait创建一个统一的AppError枚举,以简化不同子系统间的错误处理。