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

news2025/2/25 6:25:41

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
请添加图片描述

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)
    }
}
  • GroundedLaunched这两个结构体没有任何字段,因此它们的大小为,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类型,编译器将发出警告
  • 这有助于提醒用户在处理潜在的错误情况时要小心,并减少可能的错误

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2304745.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【网络编程】广播和组播

数据包发送方式只有一个接受方&#xff0c;称为单播。如果同时发给局域网中的所有主机&#xff0c;称为广播。只有用户数据报(使用UDP协议)套接字才能广播&#xff1a; 广播地址以192.168.1.0 (255.255.255.0) 网段为例&#xff0c;最大的主机地址192.168.1.255代表该网段的广…

Lecture 1 - AI Systems (Overview)

一、Machine Learning Approach标准机器学习流程 • Train ML algorithm&#xff08;训练机器学习算法&#xff09;&#xff1a;基于收集的数据训练机器学习模型。 二、Machine Learning for Adaptation&#xff08;适应性机器学习&#xff09; 加入了数据更新和自动化的部分…

Ansible 学习笔记

这里写自定义目录标题 基本架构文件结构安装查看版本 Ansible 配置相关文件主机清单写法 基本架构 Ansible 是基于Python实现的&#xff0c;默认使用22端口&#xff0c; 文件结构 安装 查看用什么语言写的用一下命令 查看版本 Ansible 配置相关文件 主机清单写法

springboot005学生心理咨询评估系统(源码+数据库+文档)

源码地址&#xff1a;学生心理咨询评估系统 文章目录 1.项目简介2.部分数据库结构与测试用例3.系统功能结构4.包含的文件列表&#xff08;含论文&#xff09;后台运行截图 1.项目简介 ​ 使用旧方法对学生心理咨询评估信息进行系统化管理已经不再让人们信赖了&#xff0c;把现…

Apache Doris:一款高性能的实时数据仓库

Apache Doris 是一款基于 MPP 架构的高性能、实时分析型数据库。它以高效、简单和统一的特性著称&#xff0c;能够在亚秒级的时间内返回海量数据的查询结果。Doris 既能支持高并发的点查询场景&#xff0c;也能支持高吞吐的复杂分析场景。 Apache Doris 最初是百度广告报表业务…

轻量级日志管理平台Grafana Loki

文章目录 轻量级日志管理平台Grafana Loki背景什么是Loki为什么使用 Grafana Loki&#xff1f;架构Log Storage Grafana部署使用基于 Docker Compose 安装 LokiMinIO K8s集群部署Loki采集Helm 部署方式和案例 参考 轻量级日志管理平台Grafana Loki 背景 在微服务以及云原生时…

《跟李沐学 AI》AlexNet论文逐段精读学习心得 | PyTorch 深度学习实战

前一篇文章&#xff0c;使用 AlexNet 实现图片分类 | PyTorch 深度学习实战 本系列文章 GitHub Repo: https://github.com/hailiang-wang/pytorch-get-started 本篇文章内容来自于学习 9年后重读深度学习奠基作之一&#xff1a;AlexNet【下】【论文精读】】的心得。 《跟李沐…

【电机控制器】FU6832S——持续更新

【电机控制器】FU6832S——持续更新 文章目录 [TOC](文章目录) 前言一、ADC二、UART三、PWM四、参考资料总结 前言 使用工具&#xff1a; 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、ADC 二、UART 三、PWM 四、参考资料 总结 本文仅仅简…

计算机视觉算法实战——产品分拣(主页有源码)

✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ ​ 1. 领域简介✨✨ 产品分拣是工业自动化和物流领域的核心技术&#xff0c;旨在通过机器视觉系统对传送带上的物品进行快速识别、定位和分类&a…

基于模块联邦的微前端架构:重构大型前端应用的模块化边界

引言&#xff1a;企业级前端的模块化困境 字节跳动广告系统采用Webpack 5模块联邦后&#xff0c;主应用构建时间从14分钟降至38秒&#xff0c;微应用独立发布频率提升至每天50次。在动态加载机制下&#xff0c;首屏资源加载体积减少79%&#xff0c;跨团队组件复用率达到92%。其…

Android之图片保存相册及分享图片

文章目录 前言一、效果图二、实现步骤1.引入依赖库2.二维码生成3.布局转图片保存或者分享 总结 前言 其实现在很多分享都是我们自定义的&#xff0c;更多的是在界面加了很多东西&#xff0c;然后把整个界面转成图片保存相册和分享&#xff0c;而且现在分享都不需要第三方&…

Linux放行端口

8080这个端口测试看telnet是不通的&#xff0c;您服务器内是否有对应的业务监听了这个端口呢&#xff1f;您到服务器内执行下&#xff1a; netstat -nltp |grep 8080 同时服务器内执行下&#xff1a; systemctl status firewalld iptables -nL 截图反馈下&#xff0c;我看下防火…

Spring Boot延迟执行实现

说明&#xff1a;本文介绍如何在Spring Boot项目中&#xff0c;延迟执行某方法&#xff0c;及讨论延迟执行方法的是事务问题。 搭建Demo 首先&#xff0c;创建一个Spring Boot项目&#xff0c;pom.xml如下&#xff1a; <?xml version"1.0" encoding"UTF-…

npm i 失败权限问题

安装完node之后, 测试全局安装一个最常用的 express 模块进行测试 失败&#xff0c;但是用管理员权限打开cmd 安装就成功。 报错如下&#xff1a; npm ERR! If you believe this might be a permissions issue, please double-check the npm ERR! permissions of the file and …

uniapp 微信小程序打包之后vendor.js 主包体积太大,解决办法,“subPackages“:true设置不生效

现在是打包的时候&#xff0c;vendor.js 的内容全部打到了主包里面&#xff0c; 说一下我的方法&#xff1a; 1. 通过发行 小程序打包 这样打包的体积是最小的&#xff0c;打包之后打开微信开发工具&#xff0c;然后再上传 2.manifest.json,在“mp-weixin”里添加代码 "…

23.2、云计算安全机制与案例分析

目录 云计算安全保护机制与技术方案云计算安全保护机制与技术方案常见云计算网络安全机制云计算安全管理与运维云计算安全综合应用案例分析 - 阿里云云计算安全综合应用案例分析 - 腾讯云云计算安全综合应用案例分析 - 华为云 云计算安全保护机制与技术方案 首先针对云计算&am…

游戏引擎学习第120天

仓库:https://gitee.com/mrxiao_com/2d_game_3 上次回顾&#xff1a;周期计数代码 我们正在进行一个项目的代码优化工作&#xff0c;目标是提高性能。当前正在优化某个特定的代码片段&#xff0c;已经将其执行周期减少到48个周期。为了实现这一目标&#xff0c;我们设计了一个…

将DeepSeek接入vscode的N种方法

接入deepseek方法一:cline 步骤1:安装 Visual Studio Code 后,左侧导航栏上点击扩展。 步骤2:搜索 cline,找到插件后点击安装。 步骤3:在大模型下拉菜单中找到deep seek,然后下面的输入框输入你在deepseek申请的api key,就可以用了 让deepseek给我写了一首关于天气的…

AI智算-k8s+SGLang实战:DeepSeek-r1:671b满血版多机多卡私有化部署全攻略

k8sSGLang实战&#xff1a;DeepSeek-r1:671b满血版多机多卡私有化部署全攻略 前言环境准备1. 模型下载2.软硬件环境介绍 正式部署1. 部署LWS API2. 通过 LWS 部署DeepSeek-r1模型3. 查看显存占用情况4. 服务对外暴露5. 测试部署效果5.1 通过 curl5.2 通过 OpenWebUIa. 部署 Ope…

【蓝桥杯单片机】第十三届省赛第二场

一、真题 二、模块构建 1.编写初始化函数(init.c) void Cls_Peripheral(void); 关闭led led对应的锁存器由Y4C控制关闭蜂鸣器和继电器 2.编写LED函数&#xff08;led.c&#xff09; void Led_Disp(unsigned char ucLed); 将ucLed取反的值赋给P0 开启锁存器 关闭锁存…