A Type Stronger than the Sum of its Components
A Type Stronger than the Sum of its Components
一种强于其组件之和的类型
A Type Stronger than the Sum of its Components 24 Sep 2026 一种强于其组件之和的类型 2026年9月24日
Have you ever written a type that you appreciated so much you still think about it? Like eating a really good meal, where if you try hard enough, you can still recall the taste in your mouth. I had a mini moment of Rust joy the other day and wanted to share the experience. TLDR: I turned an enum with N variants into N types. Nothing earth-shattering, but it made my life better. 你是否曾写过一种让你回味无穷、至今仍念念不忘的类型?就像享用了一顿极其美味的佳肴,只要努力回想,依然能记起那份滋味。前几天,我在 Rust 开发中体验到了这种小小的快乐,想与大家分享。简而言之:我将一个包含 N 个变体的枚举(enum)拆分成了 N 个独立的类型。这虽然算不上什么惊天动地的创举,但确实让我的生活变得更轻松了。
Specifically, std::path::Component is an enum that you can get from any std::path::Path reference. Where a path can be viewed as an iterator of components. To give you an example /tmp/hello is [Component::RootDir, Component::Normal("tmp"), Component::Normal("hello")]. This enum is very handy for decomposing and working with paths, but an interface that takes one component that could be any of those variants is overly broad and not terribly useful.
具体来说,std::path::Component 是一个可以从任何 std::path::Path 引用中获取的枚举。路径可以被视为组件的迭代器。举个例子,/tmp/hello 可以表示为 [Component::RootDir, Component::Normal("tmp"), Component::Normal("hello")]。这个枚举在分解和处理路径时非常方便,但如果一个接口接收的是可能为任意变体的组件,那么它的范围就太宽泛了,实用性并不高。
In a library where I work with paths a lot I made owned structs for each of those component types so that I could write a function signature like this: 在一个我频繁处理路径的库中,我为这些组件类型分别创建了拥有所有权的结构体(owned structs),这样我就可以编写如下的函数签名:
impl AbsPath {
// ...
pub(crate) fn join_normal(&self, path: &NormalComponent) -> AbsPath {
AbsPath(self.as_ref().join(path.as_ref()))
}
}
Where NormalComponent is a struct NormalComponent(OsString) that is guaranteed to come from a Component::Normal variant. In the example above, I’m using properties of this type to guarantee that joining it to a path that is already absolute will produce a path that is also absolute. It might not sound earth-shattering, but prior to that, the alternative was something like: pub(crate) fn join_normal(&self, path: &OsStr) -> AbsPath. But an OsStr could be anything. It could contain .. or be an absolute path (in which case, the join API replaces the target).
其中 NormalComponent 是一个结构体 NormalComponent(OsString),它保证了其来源必然是 Component::Normal 变体。在上面的例子中,我利用该类型的属性来确保:将其连接到一个已经是绝对路径的路径上时,产生的结果也必然是绝对路径。这听起来可能不算什么,但在那之前,替代方案通常是这样的:pub(crate) fn join_normal(&self, path: &OsStr) -> AbsPath。然而 OsStr 可能是任何东西,它可能包含 ..,也可能是一个绝对路径(在这种情况下,join API 会直接替换掉目标路径)。
Another use case is using it to represent the entries inside a directory. In hindsight, it’s such an obvious move: Take an existing, well-designed enum and make a type for each of the variants it can hold. If it’s useful to know you have 1 of N possible things (an enum), it’s probably also useful to know you have 1 very specific thing that can also fit into that enum. An enum is also known as a “sum type.” So another way to think of this is if it’s useful to have a sum type, it’s also useful to have the individual components of that type. 另一个用例是使用它来表示目录中的条目。事后看来,这是一个显而易见的做法:取一个现有的、设计良好的枚举,并为其可以持有的每个变体创建一个类型。如果知道你拥有 N 种可能事物中的一种(枚举)很有用,那么知道你拥有其中某一个非常具体的事物通常也很有用。枚举也被称为“和类型”(sum type)。因此,换个角度思考:如果拥有一个和类型很有用,那么拥有该类型中的各个独立组件同样很有用。
Prior to this abstraction, I produced a range of other types: AbsPath - A path that is absolute-ized; RelativePath - You guessed it, a path that is relative; CanonicalPath - A path that has been canonicalize-d. I found these useful, but still overly broad. A weird thing with working with paths is that they represent a lexical value and a physical location, and the two can be different. A path that is lexically relative might be a symlink on disk with an absolute target. And trying to normalize or transform paths can have weird consequences.
在进行这种抽象之前,我已经创建了一系列其他类型:AbsPath(绝对化路径)、RelativePath(你猜对了,相对路径)、CanonicalPath(规范化路径)。我发现这些类型很有用,但仍然过于宽泛。处理路径时一个奇怪的地方在于,它们既代表词法值又代表物理位置,而这两者可能并不一致。一个词法上的相对路径在磁盘上可能是一个指向绝对路径的符号链接。尝试规范化或转换路径可能会产生奇怪的后果。
Like if you try to get metadata from a file at /path/to/location/skipped/.. it will fail if skipped does not exist or is not a directory. However, if you canonicalize the path first, that will succeed and produce /path/to/location, which will not fail when you try to get metadata from it. An absolute path is not normalized, so it can have .. (ParentDir) and . (CurDir) in it. But if you don’t know how the path will be used (in my library, I don’t know why someone is asking for facts about that given path). You cannot safely normalize those values unless you’ve resolved their physical parent.
例如,如果你尝试从 /path/to/location/skipped/.. 获取文件元数据,如果 skipped 不存在或不是目录,操作就会失败。然而,如果你先对路径进行规范化,操作就会成功并生成 /path/to/location,此时再获取元数据就不会失败。绝对路径并不等同于规范化路径,因此它可能包含 .. (ParentDir) 和 . (CurDir)。但如果你不知道路径将如何使用(在我的库中,我不知道用户为何要查询该路径的相关信息),你就无法安全地规范化这些值,除非你已经解析了它们的物理父目录。
That’s because /path/to/location/skipped from above could also be a symlink to a completely different absolute path, which needs to be resolved before the “apply .. to fold parent directory” happens. And to make matters worse, Windows has special paths that change the behavior of lookups. So paths that start with \\?\ like \\?\C:\windows treat . and .. as literal values. That means, when you run a path through std::path::canonicalize it returns a path with this syntax. Which also means that it is unsafe to call canonicalize(canonicalize(&path).join(&other)) on Windows. If &other contains a .., it will produce a verbatim lookup that will likely fail.
这是因为上面提到的 /path/to/location/skipped 也可能是一个指向完全不同绝对路径的符号链接,在执行“应用 .. 以折叠父目录”之前,必须先解析该链接。更糟糕的是,Windows 拥有会改变查找行为的特殊路径。例如以 \\?\ 开头的路径(如 \\?\C:\windows)会将 . 和 .. 视为字面值。这意味着,当你通过 std::path::canonicalize 处理路径时,它会返回带有这种语法的路径。这也意味着在 Windows 上调用 canonicalize(canonicalize(&path).join(&other)) 是不安全的。如果 &other 包含 ..,它将产生一个逐字查找(verbatim lookup),这很可能会失败。
Thankfully, the Component parsing is consistent here, so it always returns a Component::ParentDir for a .. rather than a Component::Normal(".."). Join safety is probably the biggest benefit I got out of this new type, but it’s also fun that I can do things like this:
值得庆幸的是,Component 解析在这里是一致的,因此对于 ..,它总是返回 Component::ParentDir,而不是 Component::Normal("..")。连接安全性(Join safety)可能是我从这种新类型中获得的最大好处,但有趣的是,我还可以做这样的事情:
fn up(position: Reached, name: ParentDirComponent) -> (Step, Reached)
Here, the up function is walking/tracing a path on disk one component at a time. Previously, this was taking an OsString, which required the programmer to be careful. This type signature forces the developer to prove to the compiler that they hold a .. component in hand before they can call this logic. Not earth-shattering either, but this level of pedantic confidence is just so…delightful here.
在这里,up 函数正在逐个组件地遍历/追踪磁盘上的路径。以前,这个函数接收的是 OsString,这要求程序员必须非常小心。而现在的类型签名强制开发者在调用此逻辑之前,必须向编译器证明他们手中确实持有一个 .. 组件。这同样算不上惊天动地,但这种严谨带来的确定性在这里简直……太令人愉悦了。
Not everyone’s taste in food or types is the same. It’s fine if you don’t like the examples I’m serving here, but I thought this was satisfying and wanted to give you some food for thought. I would love to hear about other satisfying type patterns you’re still savoring. 每个人对食物或类型的品味不尽相同。如果你不喜欢我这里提供的示例也没关系,但我认为这很令人满意,并希望能为你提供一些思考的素材。我很乐意听听你还在回味的其他令人满意的类型模式。
An AI disclaimer: Gen AI coding tools, I also code a lot of stuff by hand, and advocate for something like a “manually coded Monday.” This src/component.rs is exclusively my meat brain child. I actually coded it while I was in a car with no internet, waiting for my kids’ soccer practice to be over. I use Grammarly (non-gen-ai mode) to help me edit my prose.
AI 免责声明:关于生成式 AI 编码工具,我本人也经常手动编写大量代码,并提倡类似“手动编码星期一”的理念。这个 src/component.rs 完全是我“肉脑”的结晶。我实际上是在一辆没有网络连接的车里,等待孩子足球训练结束时编写的。我使用 Grammarly(非生成式 AI 模式)来帮助我编辑文稿。