Hotdry.
compilers

Rust 中的「解析而非验证」:类型驱动设计的工程实践

深入解析 Rust 中通过类型系统设计在编译期捕获错误的「Parse, Don't Validate」模式。

在 Rust 编程语言社区中,有一个标签名为「parse-dont-validate」,它指向一篇关于避免验证函数、在类型层面编码不变量文章的原版使用 Haskell 编写,对不熟悉函数式编程范式的初学者来说可能不太友好。本文将以 Rust 为中心,深入探讨这一强大的类型驱动设计模式。

从除零错误说起

让我们从一个简单的除法函数开始:

fn divide(a: i32, b: i32) -> i32 {
    a / b
}

b 为零时,这个函数会导致 panic。在动态语言中,这类错误往往只在运行时才会暴露。Rust 的类型系统提供了更优雅的解决方案。一种常见做法是返回 Option<f32>,通过模式匹配处理失败情况:

fn divide_floats(a: f32, b: f32) -> Option<f32> {
    if b == 0 { None } else { Some(a / b) }
}

这种做法虽然可行,但本质上是在「削弱」返回类型 —— 告诉调用者函数可能失败,必须处理 None 情况。有没有更好的方法?

newtype 模式:把验证前移到编译期

让我们换个思路:与其削弱返回类型,不如加强参数类型。通过 newtype 模式创建一个永远不为零的浮点数类型:

mod nonzero {
    pub struct NonZeroF32(f32);
    
    impl NonZeroF32 {
        pub fn new(n: f32) -> Option<NonZeroF32> {
            if n == 0 { None } else { Some(NonZeroF32(n)) }
        }
    }
}

由于字段是私有的,外部代码无法直接构造 NonZeroF32,只能通过返回 Option 的构造函数。这意味着任何持有 NonZeroF32 实例的代码都 garantuee 其值不为零:

fn divide_floats(a: f32, b: NonZeroF32) -> f32 {
    a / b.0
}

这个模式的核心思想是:验证发生在类型转换的那一刻,之后所有使用该值的地方都不需要再次检查。考虑一个二次方程求根函数,使用 Option 版本需要在每个调用处重复检查,而 NonZeroF32 版本只需在构造时检查一次。

让非法状态不可表示

类型驱动设计的第一个核心原则是:让非法状态不可表示。以 NonZeroF32 为例,「为零」这一非法状态根本无法用该类型表示。构造函数的失败返回值确保了这一点。

这比单纯的验证函数更安全。如果只使用 is_nonzero(f32) -> bool 验证,代码中可能存在漏洞 —— 某处忘记调用验证,或者验证被意外移除。但 NonZeroF32 从根本上杜绝了这种情况。

第二个原则是:越早证明不变量越好。安全研究领域有个概念叫「霰弹枪解析」,指验证代码散落在处理逻辑中,缺乏系统性保障。这种做法容易导致 CVE-2016-0752 等安全漏洞,攻击者利用路径遍历符 .. 读取任意文件。通过在解析阶段一次性完成全面验证,可以从根本上消除这类风险。

工程实践中的类型驱动设计

事实上,你已经在使用这种模式。标准库中的 String 就是一个典型例子 —— 它本质上是 Vec<u8> 的 newtype,其构造方法 String::from_utf8 包含了 UTF-8 验证逻辑。与其拿着 Vec<u8> 四处传递并反复验证,不如直接解析成 String,类型系统会保证其有效性。

serde_json 中,这种差异更加明显。验证式写法需要多次 unwrap—— 一次检查 JSON 是否有效,一次检查字段是否存在。而通过 Deserialize 派生宏反序列化为具体类型后,这些保证在编译期就确立了:字段必然存在,类型必然匹配,索引永远合法。

实践建议

对于 API 设计,不要被原始类型束缚手脚。接受 bool 的函数不一定就要在结构体中存储 bool,定义一个语义更丰富的枚举会让代码更清晰:

enum LightBulbState {
    Off,
    On,
}

struct App {
    state: LightBulbState,
}

如果某个返回 Result<(), MyError> 的函数体没有任何副作用,那很可能应该用解析式设计 —— 将输入转换为更具结构性的数据类型。Rust 的类型系统是强大的盟友,善用它能让代码更健壮、更清晰。

资料来源:https://harudagondi.space/blog/parse-dont-validate-and-type-driven-design-in-rust

查看归档