【Rust中级教程】2.9. API设计原则之显然性(obvious) :文档与类型系统、语义化类型、使用“零大小”类型

发布时间:2026/7/24 20:45:38

【Rust中级教程】2.9. API设计原则之显然性(obvious) :文档与类型系统、语义化类型、使用“零大小”类型 2.9. API设计原则之显然性(obvious)文档与类型系统、语义化类型、使用“零大小”类型2.9.1. 文档与类型系统用户可能不会完全理解API的所有规则和限制。所以你写的API应该让你的用户易于理解并且难以用错。通过Rust的文档与类型系统我们可以尽量实现这个需求。2.9.2. 文档让API透明化的第一步就是写出好的文档。写出好的文档有这么几点要求1. 清楚的记录清楚的记录可能出现的意外情况或它依赖于用户执行超出类型签名要求的操作。例如何时会发生panic、何时返回错误。如果使用了unsafe函数那么需要写明用户需要什么条件才能安全地调用这个函数。看个例子/// 除法运算返回两个数的结果 /// /// # Panics /// /// 如果除数为0该函数会发生 panic。 /// /// # 示例 /// /// /// let result divide(10, 2); /// assert_eq!(result, 5); /// pub fn divide(dividend: i32, divisor: i32) - i32 { // ...此处省略 }这里我们把会发生恐慌的情况写进去了2. 包含端到端的用例在crate或module级别要包含端到端的用例而不是针对特定的类型或方法。这么做的好处是让用户了解这些内容是如何组合到一起的对API的整体结构有一个相对清晰的理解从而让开发者快速了解到各方法和类型的功能以及在哪里使用。在你提供了端到端的用例之后用户就可以把这段代码复制粘贴到自己的项目里相当于给用户提供了一个定制化使用的起点。举个例子假设我们有一个math_utilscrate它提供了一些数学运算功能包括基本的加法、减法和一个复杂的计算函数。这里每个函数的文档注释我就只简单写功能了但是你自己在写的时候一定要写好每个函数的文档注释。// lib.rs (crate 根模块) pub mod math_utils { /// 计算两个数的和 pub fn add(a: i32, b: i32) - i32 { a b } /// 计算两个数的差 pub fn subtract(a: i32, b: i32) - i32 { a - b } /// 执行复杂的数学运算如 a * b (a - b) pub fn complex_calculation(a: i32, b: i32) - i32 { (a * b) subtract(a, b) } } // --- 端到端用例crate 级别文档测试 --- /// /// use my_crate::math_utils; /// /// fn main() { /// let sum math_utils::add(10, 5); /// let difference math_utils::subtract(10, 5); /// let result math_utils::complex_calculation(10, 5); /// /// println!(Sum: {}, sum); // 15 /// println!(Difference: {}, difference); // 5 /// println!(Complex Calculation Result: {}, result); // 55 /// } /// 3. 组织好文档利用模块来将语义相关的项目进行分组。然后使用内部文档链接将这些项相互连接起来。有时候你可以考虑使用#[doc(hidden)]这个注解标记那些不打算公开但出于遗留的原因需要的接口部分避免弄乱文档。看个例子/// 一个简单的模块包含一些用于内部使用的函数和结构体。 pub mod internal { /// 一个用于内部计算的辅助函数。 #[doc(hidden)] pub fn internal_helper() { // 内部计算的具体实现... } /// 一个仅用于内部使用的结构体。 #[doc(hidden)] pub struct InternalStruct { // 结构体的字段和方法... } }internal_helper()函数和InternalStruct结构体都是只供内部使用的。给它们标注了#[doc(hidden)]它们的文档注释就不会出现在生成的文档注释中4.尽可能地丰富文档有时候需要解释一些内容和概念你就可以添加链接到外部资源。比如相关的规范文件RFC、博客、白皮书...在顶层文档中需要引导用户了解常用的模块、trait、类型和方法。一些有关文档内容的注解- 使用#[doc(cfg(..))]突出显示仅在特定配置下可用的项这样用户就能快速了解为什么在文档中列出的某个方法不可用。- 使用#[doc(alias ...)]可以让用户以其他名称搜索到类型和方法例子1://! 这是一个用于处理图像的库。 //! //! 这个库提供了一些常用的图像处理功能例如 //! - 读取和保存不同格式的图像文件 [Image::load] [Image::save] //! - 调整图像的大小、旋转和裁剪 [Image::resize] [Image::rotate] [Image::crop] //! - 应用不同的滤镜和效果 [Filter] [Effect] //! //! 如果您想了解更多关于图像处理的原理和算法您可以参考以下的资源 //! - [数字图像处理](https://book.douban.com/subject/5345798/)一本经典的教科书介绍了图像处理的基本概念和方法。 //! - [Learn OpenCV](https://learnopencv.com/)一个网站提供了很多用OpenCV实现图像处理功能的教程和示例代码。 //! - [Awesome Computer Vision](https://github.com/jbhuang0604/awesome-computer-vision)一个GitHub仓库收集了很多计算机视觉相关的资源和项目。 /// 一个表示图像的结构体 #[derive(Debug, Clone)] pub struct Image { // ... } // ...这里使用到了外部链接可以看到外部链接的格式是[你想展示在文档中的字](链接)这就是标准的markdown格式只要是写过自述文件的人肯定都非常熟悉。例子2:impl Image { // ... // ... #[doc(alias 读取)] #[doc(alias 打开)] pub fn loadP: AsRefPath(path: P) - ResultSelf, Error { // ... } // ... }使用了#[doc(alias 读取)]和#[doc(alias 打开)]这两个注释这样在文档中搜索“读取”和“打开”时就能搜到这个函数。例子3:/// 一个只在启用了 foo 特性时才可用的结构体。 #[cfg(feature foo)] #[doc(cfg(feature foo))] pub struct Foo; impl Foo { /// 一个只在启用了 foo 特性时才可用的方法。 #[cfg(feature foo)] #[doc(cfg(feature foo))] pub fn bar(self) { // ... } } fn main() { println!(Hello, world!); }#[cfg(feature foo)]只有当启用了foo特性时Foo结构体及其方法bar才会包含在最终的编译产物中。#[doc(cfg(feature foo))]在API说明中标注该结构体和方法依赖foo特性让使用者知道它们并非默认可用。2.9.3. 类型系统我们使用Rust的类型系统可以确保- 接口明显- 自我描述- 难以被误用语义化类型有一些值具有超过它表面的意义的比如说1和0可以代表男和女。这时候我们就可以添加类型来表示值的意义。看例子fn processData(dryRun: bool, overwrite: bool, validate: bool) { // 处理数据的逻辑 }这个函数的3个参数都是布尔类型很容易记混用户极有可能错误地使用为了解决这个问题我们可以创建3个类型并让参数是3个不同的类型enum DryRun { Yes, No, } enum Overwrite { Yes, No, } enum Validate { Yes, No, } fn processData(dryRun: DryRun, overwrite: Overwrite, validate: Validate) { // 处理数据的逻辑 }把3个布尔类型变成3个枚举类型用户在调用的时候就会写processData(DryRun::Yes, Overwrite::No, Validate::Yes)这样更加的清晰明了。使用“零大小”类型来表示关于类型实例的特定事实举个例子假设我们有一个结构体Rocket它有方法launch用于发射这个火箭没有处于已发射状态时调用这个方法肯定是没有问题的。但是如果火箭已经处于已发射状态了就不能再使用发射方法了。同样的在火箭发射后我们能控制火箭加速或减速但在地面不行。// 定义不同的火箭状态 struct Grounded; struct Launched; // 颜色枚举 enum Color { White, Black, } // 质量结构体使用 newtype 模式封装 u32 struct Kilograms(u32); // 泛型火箭结构体带有默认状态 Grounded struct RocketStage Grounded { stage: std::marker::PhantomDataStage, } // 为 Grounded 状态的 Rocket 实现 Default impl Default for RocketGrounded { fn default() - Self { Self { stage: Default::default(), } } } // 为 Grounded 状态的 Rocket 实现方法 impl RocketGrounded { pub fn launch(self) - RocketLaunched { Rocket { stage: Default::default(), } } } // 为 Launched 状态的 Rocket 实现方法 impl RocketLaunched { pub fn accelerate(mut self) {} pub fn decelerate(mut self) {} } // 为所有状态的 Rocket 实现通用方法 implStage RocketStage { pub fn color(self) - Color { Color::White } pub fn weight(self) - Kilograms { Kilograms(0) } }Grounded和Launched这两个结构体没有任何字段因此它们的大小为零Rust编译器不会为它们分配内存空间。它们仅用于标记Rocket处于哪种状态而不需要额外的存储开销。我们定义了Rocket结构体它带有一个泛型参数Stage该参数默认是Grounded。在定义中我们还使用了std::marker::PhantomDataT它是零大小类型 (ZST, Zero-Sized Type)它在编译期影响类型系统但运行时不会占用内存。launch方法仅在RocketGrounded实例上可用。launch()被调用后会返回一个RocketLaunched表示火箭已经进入发射状态。RocketLaunched不再有launch()方法确保无法重复发射。accelerate方法代表加速decelerate方法代表减速这些方法只对RocketLaunched实例有效防止在Grounded状态下加速或减速。有些方法在任何状态下都可以使用我们就写在implStage RocketStage这个块里即可。#[must_use]注解将#[must_use]注解添加到类型、trait或函数中之后如果用户的代码接收到该类型或trait的元素或调用了该函数并且没有明确处理它编译器将发出警告。看一个例子#[must_use] fn process_data(data: Data) - Result(), Error { // ... Ok(()) }我们使用#[must_use]注解将process_data函数标记为必须使用其返回值如果用户在调用该函数后没有显式处理返回的Result类型编译器将发出警告这有助于提醒用户在处理潜在的错误情况时要小心并减少可能的错误

相关新闻