HeadlinesBriefing favicon HeadlinesBriefing.com

在 Rust 中解析而非验证:非空向量

Hacker News •
×

与许多程序员一样,我发现 Alexis King 的“解析而非验证”文章非常有趣,因为它为一种似乎熟悉且重要的习惯命名,这是一种我过去观察和使用过但从未明确命名过的模式。本文是对“解析而非验证”模式在 Rust 编程语言中的应用(原文章使用的是 Haskell)的回顾。我特别感兴趣的是在 Rust 标准库和其他知名项目中寻找这种模式的教育性示例。在不重复原文(请先阅读原文!)的情况下,这里是它的核心内容。考虑经典的 Vec;它的第一个方法返回 Option<&T>。为什么?因为向量不保证有任何元素,那么如果在空向量上调用 first 方法该怎么办?在这种情况下,返回 Option 是 Rust 中的惯用做法 [1],提供了方便的语法糖来接受返回 Option 的函数的结果并决定下一步操作。那么问题出在哪里?想象一下,我们有一个函数用于从环境变量中读取一些配置路径,同时强制执行向量不能为空的约束:use anyhow::{Result, ensure}; fn get_configuration_directories() -> Result<Vec<Path Buf>> { let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?; let directories: Vec<Path Buf> = value.split(',').map(str::trim).map(Path Buf::from).collect(); ensure!(!directories.is_empty(), "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!("already checked that CONFIG_DIRS is non-empty"); } Ok(()) } 一旦 get_configuration_directories 返回成功的结果,我们就保证了向量不为空。然而,如果我们想获取这个向量中的第一个元素,我们必须使用 first 方法,该方法返回 Option<&T>。因此,我们再次被迫处理一个可能为空的情况(即选项为 None)。正如原文所指出的,这有几个问题,涉及代码清晰度、潜在的性能影响以及一个定时炸弹,如果 get_configuration_directories 中的约束条件发生变化,问题就会显现。核心问题在于,Vec 从根本上是一个可以为空的类型;我们可以在所有相关代码上写一个“这个向量不能为空,小狗保证!”的注释,但这并没有得到任何形式的检查。一种“非空”向量类型 解决方案是利用类型系统来强制执行新建立的约束。我们可以使用一个单独的类型来表示“一个不能为空的向量”;事实上,这种类型已经存在于几个 Rust 依赖项中——例如 nonempty: pub struct Non Empty<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 { Non Empty { head, tail: Vec::new() } } pub const fn first(&self) -> &T { &self.head } 这个依赖项的其余部分致力于让 Non Empty 尽可能像一个正常的 Vec,实现许多有用的 trait,以及转换函数,例如:pub fn from_vec(mut vec: Vec<T>) -> Option<Non Empty<T>> { if vec.is_empty() { None } else { let head = vec.remove(0); Some(Non Empty { head, tail: vec }) } } 让我们看看如果 get_configuration_directories 函数返回一个 Non Empty 而不是一个普通的 Vec,会是什么样子:fn get_configuration_directories() -> Result<Non Empty<Path Buf>> { let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?; let directories = value.split(',').map(str::trim).map(Path Buf::from).collect(); let Some(directories) = Non Empty::from_vec(directories) else { bail!("CONFIG_DIRS cannot be empty"); }; Ok(directories) } 请注意这里使用 Non Empty::from_vec——这就是建立约束的地方。现在...