为了学习Rust,阅读了github上的Rust By Practice电子书,本文章只是用来记录自己的学习过程,感兴趣的可以阅读原书,希望大家都能掌握Rust!
好的代码会说话,好的程序员不写注释,这些都是烂大街的“编程界俚语”。但是,如果你真的遇到一个不写注释的项目或程序员,那一定会对它/他“刮目相看”。
在之前的章节我们学习了包和模块如何使用,在此章节将进一步学习如何书写文档注释,以及如何使用 cargo doc
生成项目的文档,最后将以一个包、模块和文档的综合性例子,来将这些知识融会贯通。
在 Rust 中,注释分为三类:
Markdown
,对项目描述、公共 API 等用户关心的功能进行介绍,同时还能提供示例代码,目标读者往往是想要了解你项目的人通过这些注释,实现了 Rust 极其优秀的文档化支持,甚至你还能在文档注释中写测试用例,省去了单独写测试用例的环节,我直呼好家伙!
显然之前的刮目相看是打了引号的,想要去掉引号,该写注释的时候,就老老实实的,不过写时需要遵循八字原则:围绕目标,言简意赅,记住,洋洋洒洒那是用来形容文章的,不是形容注释!
代码注释方式有两种:
//
fn main() {
// 我是Sun...
// face
let name = "sunface";
let age = 18; // 今年好像是18岁
}
如上所示,行注释可以放在某一行代码的上方,也可以放在当前代码行的后方。如果超出一行的长度,需要在新行的开头也加上 //
。
当注释行数较多时,你还可以使用块注释
/* ..... */
fn main() {
/*
我
是
S
u
n
... 淦,好长!
*/
let name = "sunface";
let age = "???"; // 今年其实。。。挺大了
}
如上所示,只需要将注释内容使用 /* */
进行包裹即可。
你会发现,Rust 的代码注释跟其它语言并没有区别,主要区别其实在于文档注释这一块,也是本章节内容的重点。
当查看一个 crates.io
上的包时,往往需要通过它提供的文档来浏览相关的功能特性、使用方式,这种文档就是通过文档注释实现的。
Rust 提供了 cargo doc
的命令,可以用于把这些文档注释转换成 HTML
网页文件,最终展示给用户浏览,这样用户就知道这个包是做什么的以及该如何使用。
///
本书的一大特点就是废话不多,因此我们开门见山:
/// `add_one` 将指定值加1
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
以上代码有几点需要注意:
lib
类型的包中,例如 src/lib.rs
中markdown
语法!例如 # Examples
的标题,以及代码块高亮pub
对外可见,记住:文档注释是给用户看的,内部实现细节不应该被暴露出去咦?文档注释中的例子,为什看上去像是能运行的样子?竟然还是有 assert_eq
这种常用于测试目的的宏。 嗯,你的感觉没错,详细内容会在本章后面讲解,容我先卖个关子。
/** ... */
与代码注释一样,文档也有块注释,当注释内容多时,使用块注释可以减少 ///
的使用:
/** `add_two` 将指定值加2
```
let arg = 5;
let answer = my_crate::add_two(arg);
assert_eq!(7, answer);
```
*/
pub fn add_two(x: i32) -> i32 {
x + 2
}
锦衣不夜行,这是中国人的传统美德。我们写了这么漂亮的文档注释,当然要看看网页中是什么效果咯。
很简单,运行 cargo doc
可以直接生成 HTML
文件,放入target/doc目录下。
当然,为了方便,我们使用 cargo doc --open
命令,可以在生成文档后,自动在浏览器中打开网页,最终效果如图所示:
非常棒,而且非常简单,这就是 Rust 工具链的强大之处。
之前我们见到了在文档注释中该如何使用 markdown
,其中包括 # Examples
标题。除了这个标题,还有一些常用的,你可以在项目中酌情使用:
unsafe
代码,那么调用者就需要注意一些使用条件,以确保 unsafe
代码块的正常工作话说回来,这些标题更多的是一种惯例,如果你非要用中文标题也没问题,但是最好在团队中保持同样的风格
除了函数、结构体等 Rust 项的注释,你还可以给包和模块添加注释,需要注意的是,这些注释要添加到包、模块的最上方!
与之前的任何注释一样,包级别的注释也分为两种:行注释 //!
和块注释 /*! ... */
。
现在,为我们的包增加注释,在 src/lib.rs
包根的最上方,添加:
/*! lib包是world_hello二进制包的依赖包,
里面包含了compute等有用模块 */
pub mod compute;
然后再为该包根的子模块 src/compute.rs
添加注释:
//! 计算一些你口算算不出来的复杂算术题
/// `add_one`将指定值加1
///
运行 cargo doc --open
查看下效果:
包模块注释,可以让用户从整体的角度理解包的用途,对于用户来说是非常友好的,就和一篇文章的开头一样,总是要对文章的内容进行大致的介绍,让用户在看的时候心中有数。
至此,关于如何注释的内容,就结束了,那么注释还能用来做什么?可以玩出花来吗?答案是Yes
.
相信读者之前都写过单元测试用例,其中一个很蛋疼的问题就是,随着代码的进化,单元测试用例经常会失效,过段时间后(为何是过段时间?应该这么问,有几个开发喜欢写测试用例 =,=),你发现需要连续修改不少处代码,才能让测试重新工作起来。然而,在 Rust 中,大可不必。
在之前的 add_one
中,我们写的示例代码非常像是一个单元测试的用例,这是偶然吗?并不是。因为 Rust 允许我们在文档注释中写单元测试用例!方法就如同之前做的:
/// `add_one` 将指定值加1
///
/// # Examples11
///
/// ```
/// let arg = 5;
/// let answer = world_hello::compute::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
以上的注释不仅仅是文档,还可以作为单元测试的用例运行,使用 cargo test
运行测试:
Doc-tests world_hello
running 2 tests
test src/compute.rs - compute::add_one (line 8) ... ok
test src/compute.rs - compute::add_two (line 22) ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.00s
可以看到,文档中的测试用例被完美运行,而且输出中也明确提示了 Doc-tests world_hello
,意味着这些测试的名字叫 Doc test
文档测试。
需要注意的是,你可能需要使用类如
world_hello::compute::add_one(arg)
的完整路径来调用函数,因为测试是在另外一个独立的线程中运行的
文档测试中的用例还可以造成 panic
:
/// # Panics
///
/// The function panics if the second argument is zero.
///
/// ```rust
/// // panics on division by zero
/// world_hello::compute::div(10, 0);
/// ```
pub fn div(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Divide-by-zero error");
}
a / b
}
以上测试运行后会 panic
:
---- src/compute.rs - compute::div (line 38) stdout ----
Test executable failed (exit code 101).
stderr:
thread 'main' panicked at 'Divide-by-zero error', src/compute.rs:44:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
如果想要通过这种测试,可以添加 should_panic
:
/// # Panics
///
/// The function panics if the second argument is zero.
///
/// ```rust,should_panic
/// // panics on division by zero
/// world_hello::compute::div(10, 0);
/// ```
通过 should_panic
,告诉 Rust 我们这个用例会导致 panic
,这样测试用例就能顺利通过。
在某些时候,我们希望保留文档测试的功能,但是又要将某些测试用例的内容从文档中隐藏起来:
/// ```
/// # // 使用#开头的行会在文档中被隐藏起来,但是依然会在文档测试中运行
/// # fn try_main() -> Result<(), String> {
/// let res = world_hello::compute::try_div(10, 0)?;
/// # Ok(()) // returning from try_main
/// # }
/// # fn main() {
/// # try_main().unwrap();
/// #
/// # }
/// ```
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Divide-by-zero"))
} else {
Ok(a / b)
}
}
以上文档注释中,我们使用 #
将不想让用户看到的内容隐藏起来,但是又不影响测试用例的运行,最终用户将只能看到那行没有隐藏的 let res = world_hello::compute::try_div(10, 0)?;
:
Rust 在文档注释中还提供了一个非常强大的功能,那就是可以实现对外部项的链接:
/// `add_one` 返回一个[`Option`]类型
pub fn add_one(x: i32) -> Option<i32> {
Some(x + 1)
}
此处的 [Option
] 就是一个链接,指向了标准库中的 Option
枚举类型,有两种方式可以进行跳转:
Command + 鼠标左键
(macOS),CTRL + 鼠标左键
(Windows)再比如,还可以使用路径的方式跳转:
use std::sync::mpsc::Receiver;
/// [`Receiver`] [`std::future`].
///
/// [`std::future::Future`] [`Self::recv()`].
pub struct AsyncReceiver<T> {
sender: Receiver<T>,
}
impl<T> AsyncReceiver<T> {
pub async fn recv() -> T {
unimplemented!()
}
}
除了跳转到标准库,你还可以通过指定具体的路径跳转到自己代码或者其它库的指定项,例如在 lib.rs
中添加以下代码:
pub mod a {
/// `add_one` 返回一个[`Option`]类型
/// 跳转到[`crate::MySpecialFormatter`]
pub fn add_one(x: i32) -> Option<i32> {
Some(x + 1)
}
}
pub struct MySpecialFormatter;
使用 crate::MySpecialFormatter
这种路径就可以实现跳转到 lib.rs
中定义的结构体上。
如果遇到同名项,可以使用标示类型的方式进行跳转:
/// 跳转到结构体 [`Foo`](struct@Foo)
pub struct Bar;
/// 跳转到同名函数 [`Foo`](fn@Foo)
pub struct Foo {}
/// 跳转到同名宏 [`foo!`]
pub fn Foo() {}
#[macro_export]
macro_rules! foo {
() => {}
}
Rust 文档支持搜索功能,我们可以为自己的类型定义几个别名,以实现更好的搜索展现,当别名命中时,搜索结果会被放在第一位:
#[doc(alias = "x")]
#[doc(alias = "big")]
pub struct BigX;
#[doc(alias("y", "big"))]
pub struct BigY;
这个例子我们将重点应用几个知识点:
lib
包(库包),它们的包根分别是 src/main.rs
和 src/lib.rs
lib
包pub use
再导出 API,并观察文档首先,使用 cargo new art
创建一个 Package art
:
Created binary (application) `art` package
系统提示我们创建了一个二进制 Package
,根据之前章节学过的内容,可以知道该 Package
包含一个同名的二进制包:包名为 art
,包根为 src/main.rs
,该包可以编译成二进制然后运行。
现在,在 src
目录下创建一个 lib.rs
文件,同样,根据之前学习的知识,创建该文件等于又创建了一个库类型的包,包名也是 art
,包根为 src/lib.rs
,该包是是库类型的,因此往往作为依赖库被引入。
将以下内容添加到 src/lib.rs
中:
//! # Art
//!
//! 未来的艺术建模库,现在的调色库
pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;
pub mod kinds {
//! 定义颜色的类型
/// 主色
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// 副色
#[derive(Debug,PartialEq)]
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
//! 实用工具,目前只实现了调色板
use crate::kinds::*;
/// 将两种主色调成副色
/// ```rust
/// use art::utils::mix;
/// use art::kinds::{PrimaryColor,SecondaryColor};
/// assert!(matches!(mix(PrimaryColor::Yellow, PrimaryColor::Blue), SecondaryColor::Green));
/// ```
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
SecondaryColor::Green
}
}
在库包的包根 src/lib.rs
下,我们又定义了几个子模块,同时将子模块中的三个项通过 pub use
进行了再导出。
接着,将下面内容添加到 src/main.rs
中:
use art::kinds::PrimaryColor;
use art::utils::mix;
fn main() {
let blue = PrimaryColor::Blue;
let yellow = PrimaryColor::Yellow;
println!("{:?}",mix(blue, yellow));
}
在二进制可执行包的包根 src/main.rs
下,我们引入了库包 art
中的模块项,同时使用 main
函数作为程序的入口,该二进制包可以使用 cargo run
运行:
Green
至此,库包完美提供了用于调色的 API,二进制包引入这些 API 完美的实现了调色并打印输出。
最后,再来看看文档长啥样:
在 Rust 中,注释分为三个主要类型:代码注释、文档注释、包和模块注释,每个注释类型都拥有两种形式:行注释和块注释,熟练掌握包模块和注释的知识,非常有助于我们创建工程性更强的项目。
如果读者看到这里对于包模块还是有些模糊,强烈建议回头看看相关的章节以及本章节的最后一个综合例子。
/* 只使用注释让下面代码工作! */
fn main() {
todo!();
unimplemented!();
assert_eq!(6, 5 + 3 + 2 + 1 )
}
文档注释会被解析为 HTML 文件,并支持 Markdown
语法。
在开始之前,我们需要创建一个新的项目用于后面的练习: cargo new --lib doc-comments
.
为 add_one
函数添加文档
// in lib.rs
/// Add one to the given value and return the value
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
我们可以使用 cargo doc --open
来生成 HTML 文件,并自动在浏览器中打开网页。
为函数 add_two
添加文档:
/** Add two to the given value and return a new value
let arg = 5;
let answer = my_crate::add_two(arg);
assert_eq!(7, answer);
*/
pub fn add_two(x: i32) -> i32 {
x + 2
}
我们还可以创建包和模块的注释,用于描述它们的功能。
首先,来为我们的库包添加一些文档注释:
注意: 必须要将包、模块注释放置在包根或模块文件的最顶部
//! # 文档注释
//!
//! 该库用于文档注释的教学
// in lib.rs
pub mod compute;
同样的,我们还可以使用块注释来达成目的:
/*! # 文档注释
该库用于文档注释的教学 */
下一步,创建一个新的模块文件 src/compute.rs
, 然后在其中添加以下注释:
//! 本模块用于处理一些复杂计算
// in compute.rs
然后运行 cargo doc --open
查看下结果。
细心的同学可能会发现之前的 add_one
和 add_tow
的文档注释中,包含了两个示例代码块.
以上示例不仅仅是作为文档用于演示你的函数该如何使用,它的另一个作用就是用于文档测试 cargo test
。
但是在这两个函数的示例中,存在错误,请修复它们并使用 cargo test
获取以下输出结果:
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests doc-comments
running 2 tests
test src/lib.rs - add_one (line 11) ... ok
test src/lib.rs - add_two (line 26) ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.55s
有时我们会期望示例的结果是一个 panic。将以下代码添加到 src/compute.rs
,并且让 cargo test
成功运行.
你只能修改注释,不要修改
fn div
// in src/compute.rs
/// # Panics
///
/// The function panics if the second argument is zero.
///
/// ```rust,should_panic
/// // panics on division by zero
/// doc_comments::compute::div(10, 0);
/// ```
pub fn div(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Divide-by-zero error");
}
a / b
}
有时我们会想要隐藏文档,但是保留文档测试
将以下代码添加到 src/compute.rs
,
// in src/compute.rs
/// ```
/// # fn try_main() -> Result<(), String> {
/// let res = doc_comments::compute::try_div(10, 0)?;
/// # Ok(()) // returning from try_main
/// # }
/// # fn main() {
/// # try_main().unwrap();
/// #
/// # }
/// ```
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Divide-by-zero"))
} else {
Ok(a / b)
}
}
然后修改以上代码已实现两个目标:
cargo doc --open
生成的网页中running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests doc-comments
running 4 tests
test src/compute.rs - compute::div (line 7) ... ok
test src/lib.rs - add_two (line 27) ... ok
test src/lib.rs - add_one (line 11) ... ok
test src/compute.rs - compute::try_div (line 20) ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.51s
Rust 为我们提供一个非常强大的特性:可以在文档注释中实现代码跳转。
将以下代码添加到 src/lib.rs
:
// in lib.rs
/// Add one to the given value and return a [`Option`] type
pub fn add_three(x: i32) -> Option<i32> {
Some(x + 3)
}
除了跳转到标准库中,我们还能跳转到项目中的其它模块。
// in lib.rs
mod a {
/// Add four to the given value and return a [`Option`] type
/// [`crate::MySpecialFormatter`]
pub fn add_four(x: i32) -> Option<i32> {
Some(x + 4)
}
}
struct MySpecialFormatter;
下面是很常用的 #[doc]
属性,该属性可以被 rustdoc
所使用。
inline
可以用于内联文档, 而不是链接到一个单独的页面。
#[doc(inline)]
pub use bar::Bar;
/// bar docs
mod bar {
/// the docs for Bar
pub struct Bar;
}
no_inline
用于防止链接到单独的页面或其它地方。
// Example from libcore/prelude
#[doc(no_inline)]
pub use crate::mem::drop;
hidden
通过这个属性让 rustdoc
不要将下面的项包含在文档中:
// Example from the futures-rs library
#[doc(hidden)]
pub use self::async_await::*;
对文档来说,rustdoc
被社区广泛采用,大家所看到的标准库文档也是基于此生成的。
doc-comments
的完整代码可以在这里找到.