ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Rust Design Patterns 之 Easy Doc Initialization:用辅助函数精简 rustdoc 示例样板代码

Rust Design Patterns 之 Easy Doc Initialization:用辅助函数精简 rustdoc 示例样板代码 文档教程【免费下载链接】patternsA catalogue of Rust design patterns, anti-patterns and idioms项目地址https://gitcode.com/gh_mirrors/pa/patterns点击查看免费下载rustdoc 是 Rust 生态中最常用的文档工具但为每一个方法编写可运行示例时构造复杂类型的样板代码往往比示例本身还长。本文讲解 Rust Design Patterns 开源图书中记录的Easy doc initialization惯用法用一个从未被调用的辅助函数包装示例代码把初始化麻烦转化为参数传递在保证示例通过编译检查的同时彻底消除每个文档示例里重复的构造代码。读完本文你将掌握如何在自己的 crate 中写出更简洁、更可维护、也更适合no_run场景的 doctest 示例。问题背景rustdoc 示例里的初始化样板代码rustdoc 的文档注释支持用 Markdown 代码块直接编写可测试的示例doctest。默认情况下cargo test会把这些代码块当作测试来编译并运行。然而一旦被文档化的类型拥有多个或复杂参数以及多个方法问题就出现了——每个方法都需要一个示例而每个示例都需要重复相同的对象构造代码。原文档给出了一个非常典型的场景一个Connection结构体包含一个String字段和一个TcpStream字段并且拥有多个方法struct Connection { name: String, stream: TcpStream, } impl Connection { /// Sends a request over the connection. /// /// # Example /// no_run /// # // Boilerplate are required to get an example working. /// # let stream TcpStream::connect(127.0.0.1:34254); /// # let connection Connection { name: foo.to_owned(), stream }; /// # let request Request::new(RequestId, RequestType::Get, payload); /// let response connection.send_request(request); /// assert!(response.is_ok()); /// fn send_request(self, request: Request) - ResultStatus, SendErr { // ... } /// Oh no, all that boilerplate needs to be repeated here! fn check_status(self) - Status { // ... } }注意上面这段代码的写法为了制造一个可以调用的connection和request作者不得不用#前缀把TcpStream::connect(...)、Connection { ... }、Request::new(...)这些构造语句藏起来rustdoc 会隐藏以#开头的行但这些行在编译测试时依然生效。一旦check_status这类方法也需要示例这四五行样板代码就要原样再复制一遍——正如代码注释里那句自嘲Oh no, all that boilerplate needs to be repeated here!哦不所有这些样板代码又得在这里重复一遍。这带来的直接问题是样板代码越多文档示例越难读示例与真实用法之间的信噪比越低维护成本也随之上升。解决方案用辅助函数包装示例原文档给出的核心思路非常巧妙与其在每个示例里都完整初始化Connection和Request不如创建一个包装用的辅助函数把这两个对象作为参数传入。示例代码里只保留真正要演示的调用逻辑struct Connection { name: String, stream: TcpStream, } impl Connection { /// Sends a request over the connection. /// /// # Example /// /// # fn call_send(connection: Connection, request: Request) { /// let response connection.send_request(request); /// assert!(response.is_ok()); /// # } /// fn send_request(self, request: Request) - ResultStatus, SendErr { // ... } }对比两个版本可以发现新版本的示例代码块内只有三行实质内容调用send_request、把结果绑定给response、断言其结果为Ok。而所有初始化工作构造Connection、Request等都被挪进了以#开头的隐藏函数声明fn call_send(connection: Connection, request: Request)中对阅读文档的人完全不可见。这套写法的要点可以归纳为声明一个接收结构体为参数的辅助函数例如fn call_send(connection: Connection, request: Request)把示例要演示的调用语句原样放进函数体内保持与真实使用场景一致用#前缀隐藏函数声明与结束的# }让渲染出的文档只显示核心调用函数内的断言、打印等语句仍然存在保持示例的真实感。关键细节隐藏行与从未被调用的断言必须向读者澄清一个容易误解的细节。原文档特别强调在上面这个示例中assert!(response.is_ok());并不会在测试时真正执行因为它位于一个从未被调用过的函数call_send内部。这正是本惯用法与普通 doctest 的本质区别普通 doctest代码块不带no_run代码块会被编译并执行断言真实生效本惯用法函数包装 隐藏行代码块会被编译检查但函数体不运行因此其中的断言只是摆设用于向读者展示预期行为而不是真正验证行为。换句话说这个技巧牺牲了运行测试的能力换来了示例代码大幅精简的收益。它本质上是把 rustdoc 示例从可运行的测试降级为可编译的演示。这个技巧为什么与no_run天然契合原文档明确指出这一惯用法最适合需要no_run的场景而且一旦采用此写法就不再需要显式添加no_run标记。理解这一点需要先弄清 rustdoc 代码块的几种模式代码块标记编译运行典型用途无标记✅✅需要被当作测试执行的完整示例no_run✅❌示例涉及网络、IO、外部依赖等无法在测试环境运行的操作ignore❌❌故意不参与测试的片段如依赖未定义类型的伪代码compile_fail期望编译失败❌演示编译错误的负面示例原文档中的Connection示例必须连接TcpStream这在 doctest 环境里并不适合真正运行因此原写法被迫使用no_run。而使用辅助函数包装之后函数体不会被执行因此不需要担心网络连接、文件 IO 等副作用代码块仍然会被cargo test编译检查语法和类型层面的正确性依然有保障于是可以直接使用无标记的普通代码块省去no_run同时保持示例简洁。从源码结构看book.toml 中[rust] edition 2024表明本书示例按 2024 edition 编译本惯用法依赖的是 rustdoc 自身的代码块标记与#隐藏行机制与 edition 无关因此在任何 edition 下都适用。优点原文档对本惯用法的优点概括得十分精炼更简洁并且避免了示例中重复的样板代码。展开来说可读性大幅提升读者看到的文档示例只包含核心 API 调用不会被构造代码淹没维护成本降低当结构体字段变化例如新增一个参数时只需修改一处辅助函数签名而无需逐个示例去改初始化语句无需no_run解决了示例需要编译但不适合运行这一常见两难零运行时负担辅助函数只存在于文档示例中不会出现在 crate 的公共 API 中。缺点与适用边界对应地原文档也诚实列出了代价示例代码不会被测试。函数体内的断言、分支逻辑都不会真正执行因此示例展示的行为得不到运行时验证编译检查仍然有效。运行cargo test时该代码块依然会被编译类型错误、未定义项等编译期问题仍会被捕获。由此得出清晰的适用边界当示例不需要断言或断言只是示意性展示时这个模式非常合适——这也是它最理想的适用场景当示例中的断言必须真实执行例如演示某个算法的输入输出关系时本惯用法不适用应当退回到完整的、可运行的 doctest 写法。讨论与替代方案#[doc(hidden)]公共辅助方法如果确实需要让断言执行原文档还给出了一种替代思路创建一个以#[doc(hidden)]标注的公共方法用它来生成辅助实例然后在 doctest 中调用它。其原理在于rustdoc 的 doctest 在编译时会把示例代码当作当前 crate 的一个外部使用方来编译因此示例只能访问 crate 的公共 API。而#[doc(hidden)]的语义是公共可见但不出现在文档中——它依然属于 crate 的公共 API示例代码可以正常调用但用户浏览生成的文档时却看不到这个方法不会因此受到困扰。这条路径与Default、构造函数等惯用法可以组合使用。例如Constructor 惯用法 中强调类型应同时实现new和Default如果Connection实现了Default就可以在辅助函数内部用Connection::default()快速构造出测试实例进一步缩短样板代码。需要构造多种复杂配置时还可以参考本书的 Builder 模式 提供的链式构造 API将其作为辅助函数内部的构造手段。两种方案的取舍可以总结为需求推荐方案示例只需展示调用方式无需运行辅助函数包装本惯用法省略no_run示例需要真实运行并验证断言完整 doctest必要时在构造环节使用辅助代码需要可调用但不污染文档的构造入口#[doc(hidden)]公共辅助方法结构复杂、参数繁多结合Default/ 构造器惯用法或 Builder 模式在本书中的位置与相关实践本篇文章位于 Idioms 章节 中编号为Easy doc initialization并在 SUMMARY.md 中与其他惯用法如Iterating over anOption、Temporary mutability、Return consumed arg on error并列。Idioms 章节开篇即点明了这套技巧背后的指导思想——代码是写给人类读的而不仅是让计算机执行的Code is there for humans, not computers, to understand.Easy doc initialization正是这一理念在文档层面的一次实践文档示例的首要读者是开发者让示例读起来清晰、聚焦比让它事无巨细地展示全部构造细节更重要。值得注意的是本书 template.md 对所有文章也提出了同样的要求示例代码应尽量可编译please try to make them compile若无法做到完整可读至少要标记为ignore。这与本惯用法编译通过、运行可选的精神一脉相承——文档示例的首要目标是准确传达 API 用法而编译检查则是它的最低保障线。如果你要为本仓库贡献或验证类似文档可以按照 README.md 的说明安装 mdbook 后在仓库根目录执行mdbook build构建图书、mdbook test验证所有示例代码的正确性。这些命令同样适用于你本地的任何 Rust 项目cargo test会统一编译并运行源码测试与 doctest从而确保本文所述写法中编译检查仍然有效这一特性真实生效。小结Easy doc initialization是一个小而实用的 rustdoc 惯用法当结构体初始化成本较高时用一个以#隐藏的辅助函数把结构体作为参数传入示例让示例只展示核心调用逻辑。它的收益是示例简洁、无重复样板代码、无需no_run代价是示例代码不再被真正运行。当你需要既展示、又验证时可以改用#[doc(hidden)]公共辅助方法或保留完整 doctest。判断标准很简单看断言是否需要真实执行——不需要就用辅助函数包装需要就老老实实完整初始化。赞分享文档教程【免费下载链接】patternsA catalogue of Rust design patterns, anti-patterns and idioms项目地址https://gitcode.com/gh_mirrors/pa/patterns点击查看免费下载相关推荐Home Assistant Habitica 变形物品实战快速上手不写一行代码把队友变雪人Home Assistant Habitica 变形物品实战快速上手不写一行代码把队友变雪人 想把队友恶搞成雪人却在 Home Assistant 里找不到文档教程智能家居物联网gh_mirrors/pa/patterns代码示例管理Rustdoc初始化与可测试文档gh_mirrors/pa/patterns代码示例管理Rustdoc初始化与可测试文档 痛点与解决方案概述 在Rust项目文档编写中复杂结构体的初始化代码文档教程Agentic-doc 终极指南5个典型使用场景代码示例Agentic doc 终极指南5个典型使用场景代码示例 Agentic doc 是一个强大的 Python 库专门用于智能文档提取和分析。这个库封装了 V上一篇终极指南pongo2模板引擎常见问题解决方案与实战技巧下一篇tusd 项目常见问题解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表