喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
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_utils
crate,它提供了一些数学运算功能,包括基本的加法、减法和一个复杂的计算函数。这里每个函数的文档注释我就只简单写功能了,但是你自己在写的时候一定要写好每个函数的文档注释。
// 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 load<P: AsRef<Path>>(path: P) -> Result<Self, 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 Rocket<Stage = Grounded> {
stage: std::marker::PhantomData<Stage>,
}
// 为 Grounded 状态的 Rocket 实现 Default
impl Default for Rocket<Grounded> {
fn default() -> Self {
Self {
stage: Default::default(),
}
}
}
// 为 Grounded 状态的 Rocket 实现方法
impl Rocket<Grounded> {
pub fn launch(self) -> Rocket<Launched> {
Rocket {
stage: Default::default(),
}
}
}
// 为 Launched 状态的 Rocket 实现方法
impl Rocket<Launched> {
pub fn accelerate(&mut self) {}
pub fn decelerate(&mut self) {}
}
// 为所有状态的 Rocket 实现通用方法
impl<Stage> Rocket<Stage> {
pub fn color(&self) -> Color {
Color::White
}
pub fn weight(&self) -> Kilograms {
Kilograms(0)
}
}
-
Grounded
和Launched
这两个结构体没有任何字段,因此它们的大小为零,Rust编译器不会为它们分配内存空间。它们仅用于标记Rocket
处于哪种状态,而不需要额外的存储开销。 -
我们定义了
Rocket
结构体,它带有一个泛型参数Stage
,该参数默认是Grounded
。在定义中我们还使用了std::marker::PhantomData<T>
,它是零大小类型 (ZST, Zero-Sized Type),它在编译期影响类型系统,但运行时不会占用内存。 -
launch
方法仅在Rocket<Grounded>
实例上可用。 -
launch()
被调用后,会返回一个Rocket<Launched>
,表示火箭已经进入发射状态。Rocket<Launched>
不再有launch()
方法,确保无法重复发射。 -
accelerate
方法代表加速,decelerate
方法代表减速,这些方法只对Rocket<Launched>
实例有效,防止在Grounded
状态下加速或减速。 -
有些方法在任何状态下都可以使用,我们就写在
impl<Stage> Rocket<Stage>
这个块里即可。
#[must_use]
注解
将#[must_use]
注解添加到类型、trait或函数中之后,如果用户的代码接收到该类型或trait的元素,或调用了该函数,并且没有明确处理它,编译器将发出警告。
看一个例子:
#[must_use]
fn process_data(data: Data) -> Result<(), Error> {
// ...
Ok(())
}
- 我们使用
#[must_use]
注解将process_data
函数标记为必须使用其返回值 - 如果用户在调用该函数后没有显式处理返回的
Result
类型,编译器将发出警告 - 这有助于提醒用户在处理潜在的错误情况时要小心,并减少可能的错误