返回

文章详情

对“解析,不要验证”的思考

Hacker News2026年9月27日 08:59

像许多程序员一样,我发现 Alexis King 的《解析,不要验证》一文非常迷人,因为它给一个看似熟悉且重要的习语命名——这是我过去在没有明确命名的情况下观察和使用过的。本文评价了将“解析,不要验证”模式应用于 Rust 编程语言的案例(原文使用 Haskell)。我特别感兴趣的是在 Rust 标准库和其他知名项目中找到该模式的教育示例。在不重复原文的情况下(请先阅读它!),这里是它的要点。考虑到尊贵的 Vec;它的第一个方法返回 Option<&T>。这是为什么呢?因为一个向量不保证有任何元素,那如何处理在空向量上调用 first 的情况呢?在这种情况下返回一个 Option 是 Rust 中的习惯用法 [1],并且有方便的语法糖来接受返回 Option 的函数的结果并决定下一步该做什么。那么问题是什么呢?想象一下我们有一个函数从环境变量中读取一些配置路径,同时强制保证列表不能为空:使用 anyhow::{Result, ensure}; fn get_configuration_directories() -> Result<Vec<PathBuf>> { let value = env::var("CONFIG_DIRS").context("无法读取 CONFIG_DIRS")?; let directories: Vec<PathBuf> = value.split(',').map(str::trim).map(PathBuf::from).collect(); ensure!(!directories.is_empty(), "CONFIG_DIRS 不能为空"); Ok(directories) } 到目前为止,一切都很好。现在让我们看一下该函数的典型用法:fn main() -> Result<()> { let config_dirs = get_configuration_directories()?; match config_dirs.first() { Some(cache_dir) => initialize_cache(cache_dir), None => unreachable!("已经检查过 CONFIG_DIRS 不是空的"), } Ok(()) } 一旦 get_configuration_directories 返回成功的结果,我们可以保证向量不是空的。然而,如果我们想获取该向量的第一个元素,我们必须使用返回 Option<&T> 的 first 方法。因此,我们再次被迫处理一个潜在的空案例(如果选项是 None)。正如原文所述,这在代码清晰度、潜在的性能影响和 get_configuration_directories 如果更改不变性时的隐患方面存在许多问题。主要问题在于 Vec 本质上是一个可以为空的类型;我们可以在所有相关代码上附加“这个不能是空的,发誓!”的注释,但没有任何东西正式检查。 一个“非空”向量的类型 解决方案是利用类型系统来强制执行新建立的不变条件。我们可以为“不能是空的向量”使用一个单独的类型;实际上,这种类型在几个 Rust crate 中已经存在——例如 nonempty:pub struct NonEmpty<T> { pub head: T, pub tail: Vec<T>, } 该类型没有允许“没有元素”的构造函数;它的 new 需要一个元素,而它的 first 方法返回 &T,而非 Option:pub const fn new(e: T) -> Self { Self::singleton(e) } pub const fn singleton(head: T) -> Self { NonEmpty { head, tail: Vec::new(), } } pub const fn first(&self) -> &T { &self.head } 该 crate 其余部分通过实现许多有用的特征以及诸如:pub fn from_vec(mut vec: Vec<T>) -> Option<NonEmpty<T>> { if vec.is_empty() { None } else { let head = vec.remove(0); Some(NonEmpty { head, tail: vec }) } } 使 NonEmpty 尽可能接近正常 Vec。 如果我们的 get_configuration_directories 函数返回 NonEmpty,而不是简单的 Vec,它看起来会是怎样的呢:fn get_configuration_directories() -> Result<NonEmpty<PathBuf>> { let value = env::var("CONFIG_DIRS").context("无法读取 CONFIG_DIRS")?; let directories = value.split(',').map(str::trim).map(PathBuf::from).collect(); let Some(directories) = NonEmpty::from_vec(directories) else { bail!("CONFIG_DIRS 不能为空"); }; Ok(directories) } 注意这里使用的 NonEmpty::from_vec——这里是设定不变性的地方。现在一个成功的结果是 NonEmpty,而不仅仅是 Vec。客户端代码看起来像这样:fn main() -> Result<()> { let config_dirs = get_configuration_directories()?; initialize_cache(config_dirs.first())?; Ok(()) } 不再需要再次检查返回值是否为空;这是通过类型系统来强制的!这就是原文中解析与验证术语的来源。当 get_configuration_directories 返回 Vec 时,它只是验证了它。但当它返回 NonEmpty 时——向量被转换为携带附加含义的另一个实体。如果我们从最通用的角度看待解析的概念——“将数据从一种格式转换为另一种”,这就合适。提到一个不那么人工的例子,Rust 对核心 POSIX 实用程序的重写在几个地方使用了 NonEmpty。

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

☕请我喝杯咖啡