概述
编程语言发展到今天,除了有语言标准之外,还有一系列其他约定俗成的标准,例如,项目管理、文档规范、编码规范等等。很多早期的编程语言则没有一个统一的标准,而 Rust 编程语言作为一个完全开源的后起之秀,在诞生之初就针对性的解决完善了这些问题!同时又充分吸收了现代编程语言的一些特性,例如,Rust 有前端编程语言标配的包管理器,类似于前端脚手架的项目管理等。
- Node.js:
npm
+ https://npmjs.org/ - Python:
pip
+ https://pypi.org/ - Ruby:
RubyGems
+ https://rubygems.org/ - Rust:
cargo
+ https://crates.io/
项目管理
以前比较古老的编程语言通常没有一个固定的项目管理工具,通常都是在发展过程中出现一些第三方的 IDE 作为各自的管理工具。Rust 编程语言作为一个完全开源的后起之秀,在诞生之初就引入了自己的项目管理工具!
管理工具
Rust 官方并没有提供一个统一的 IDE 作为项目管理的工具,但是,提供了一系列适用于各个平台的开发辅助命令行工具来帮助我们管理、编译 Rust 代码项目以及 Rust 开发环境。
Cargo
Rust 官方提供了一个名为 Cargo 的命令行项目管理工具,绝大多数情况下,我们都是使用 cargo
命令及 *.toml
的配置文件来管理编译项目。Rust 项目源码的基本结构就是针对 Cargo 这个管理工具来定的!
其他工具
此外,Rust 官方还提供了一系列其他工具,例如,代码格式化的 rustfmt
、代码检查工具 clippy
等等。详细介绍参见博文 Rust 之一 基本环境搭建、各组件工具的文档、源码、配置。
项目结构
Rust 编程中一个项目通常称为一个 Package,并将 Package 分成多个 Crate, Crate 再进一步分成多个 Module。并规定了如何通过绝对路径或相对路径从一个 Module 导入另一个 Module 中定义的内容的方式。
- 一个 Package 中至少包含一个 Crate(可以是一个 Library Crate 也可以是一个 Binary Crate)
- 一个 Package 中最多只能包含一个 Library Crate
- 一个 Package 中可以包含任意多个 Binary Crate
- 一个 Package 会包含一个 Cargo.toml 文件
- 一个 Crate 可以包含任意多个 Module
- Module 可以嵌套
注意,根据传统编程语言的称呼,我们通常会把其他人共享的代码称为包(Package)或库(Library),但 Rust 社区中习惯上都是叫 Crate。因此,在 Rust 中我们平时说的包、库、Crate 很多时候都是一个东西,但实际上他们还是有些区别的!
Package
Package(中文名 包
)是 Cargo 进行项目管理的基本单位,它是一个提供了一系列功能的一个或者多个 Crate 的集合。Rust 官方对 Package 的目录结构进行了规范化,其标准的目录结构如下所示:
.
├── Cargo.lock # 在执行构建后自动生成,固定名字,不能更改
├── Cargo.toml # 每个 Package 必有一个,固定名字,不能更改
├── src/ # Cargo 要求源文件位于 src 目录中。顶级项目目录仅用于存放 README 文件、许可证信息、配置文件以及与代码无关的其他任何内容。
│ ├── lib.rs # 这个就是 Library Crate 的入口,名字必须为 lib.rs,这个源码文件称为 Library Crate 的 crate root。
│ ├── single_file_module.rs # 这个就是一个 Module,只有一个文件
│ ├── multi-file-module/ # 可以使用一个子目录来组织多个 Module
│ │ ├── mod.rs
│ │ ├── a_module_file.rs
│ │ └── another_module_file.rs
│ ├── main.rs # 这个就是 Binary Crate,名字必须为 main.rs,被称为 Binary Crate 的 crate root
│ └── bin/ # 如果有多个 Binary Crate,则必须将他们放到 bin 目录下
│ ├── named-executable.rs # 这也是个 Binary Crate
│ ├── another-executable.rs # 这也是个 Binary Crate
│ └── multi-file-executable/ # 可以使用一个子目录来组织 Binary Crate 及其 Module
│ ├── main.rs # 这也是个 Binary Crate
│ └── some_module.rs # 这个就是一个 Module
├── benches/ # 固定名字,不能更改。可选,很多 Package 没有
│ ├── large-input.rs
│ └── multi-file-bench/
│ ├── main.rs
│ └── bench_module.rs
├── examples/ # 固定名字,不能更改。可选,很多 Package 没有
│ ├── simple.rs
│ └── multi-file-example/
│ ├── main.rs
│ └── ex_module.rs
├── tests/ # 固定名字,不能更改。可选,很多 Package 没有
│ ├── some-integration-tests.rs
│ ├── common/mod.rs -> shared module for integration tests
│ └── multi-file-test/
│ ├── main.rs
│ └── test_module.rs
└── 其他目录 # 根据自己的需要,可以选择一些其他目录。例如,创建一个 crates 目录存放本地的 Crate
-
这个是目录结构实际上是 Rust 官方提供的 Cargo 这个项目管理工具的要求,如果不使用 Cargo,则目录结构可以变动
-
最简单的 Package 就只包含一个 Binary Crate 或 Library Crate,因此,在 Rust 中,很多时候一个 Package 就是一个 Crate。使用
cargo new
命令就可以自动生成一个只含一个 Crate 的 Package。 -
官方建议使用全小写来作为名字,这是因为 Rust 采用 蛇形命名法(snake_case) 或 串式命名法(kebab-case) 命名规范。
-
Package 的名字不能与 Rust 库内建的同名
Cargo.toml
Cargo.toml 是 Cargo 引入的一个 Manifest 文件,每一个 Package 都必须包含一个 Cargo.toml 文件,它包含编译当前 Package 所需的元数据,用以阐述如何去构建其中的各个 Crate。它是由开发者手动编写的项目的配置清单,描述了项目信息和依赖项等。
# [package] 是当前这个 Package 的配置。[xxx] 这个在 TOML 语法中被称为 表(也被称为哈希表或字典)是键值对的集合。
[package]
name = "demo"
version = "0.1.0"
edition = "2021"
# [dependencies] 是当前这个 Package 的的依赖。
[dependencies]
rand = "0.8.0"
-
Rust 中的绝大多数工具的默认配置文件都是使用 TOML(Tom’s Obvious Minimal Language)这种类型的配置文件格式。
- 以行为一个处理单位,称为 键值对,格式为
键名 = 键值
,键名和键值周围的空白会被忽略。- 键值的类型如下所示
- 字符串:多行基本字符串
"xxx"
可以任意多行,包含转义字符等;字面量字符串'xxxx'
只能一行,不能含转义字符;多行字面量字符串'''xxxx'''
可以多行,但不能含转义字符 - 整数:整数是纯数字;正数可以以加号为前缀;负数以减号为前缀。可以在数字之间用下划线来增强可读性,例如
int6 = 5_349_221
。带有0x
前缀的十六进制,带有0o
前缀的八进制,带有0b
前缀的二进制 - 浮点数。小数(3.14)、指数(5e+22)、无穷(+inf、-inf)、非数(+nan、-nan)
- 布尔值。true 和 fasle
- 坐标日期时刻
- 各地日期时刻
- 各地日期
- 各地时刻
- 数组。以方括号包裹,里面的值以逗号风格,例如 [ 1, 2, 3 ]。数组可以跨行
- 内联表
- 字符串:多行基本字符串
- 键名可以是裸的,引号引起来的,或点分隔的。例如,
abc = "xxx"
或"abc" == "xxx"
或a.b.c = 11
。需要注意,点分隔形式实际上是表示下面的子节点!
- 键值的类型如下所示
- 以
#
开头作为注释 - TOML 是大小写敏感的。
- TOML 文件必须是合法的 UTF-8 编码的 Unicode 文档。
- 空白是指制表符(0x09)或空格(0x20),换行是指 LF(0x0A)或 CRLF(0x0D 0x0A)
[xxx]
表示后续键值对的集合,在 TOML 语法中被称为表(也被称为哈希表或字典)。[[xxx]]
称为表数组,其中 xxx 是表头,多个[[xxx]]
组成一个表
- 以行为一个处理单位,称为 键值对,格式为
-
Cargo.toml 包含如下字段
- cargo-features: 当前为不稳定特性,仅适用于 Nightly 通道
- [package]: 定义 Package 的基本信息
- name: 名字,最少必须有该字段。如果是要发布到 crate.io 的包,则需要额外几个字段,例如 version 字段
- 必须只使用字母、数字或
-
或_
,并且不能为空
- 必须只使用字母、数字或
- version: 版本
- Rust 使用语义化版本号
x.y.z
格式
- Rust 使用语义化版本号
- authors: 作者。以
[xxx, yyy]
的形式列出所有作者,例如,authors = ["zcs", "ZCShou <72115@163.com>"]
- edition: Rust 语言版本
- rust-version: 最小支持的 Rust 版本。第一个支持这个字段的 Cargo 版本是在 Rust 1.56.0中发布的。在较旧的版本中,该字段将被忽略,并且 Cargo 将显示一个警告。
- description: 描述
- documentation: 文档 URL
- readme: README 文件的路径
- homepage: 主页 URL
- repository: 源码仓库 URL
- license: 包的 license.
- license-file: 许可证文本的路径
- keywords: 关键字
- categories: 包的分类
- workspace: 包的工作区路径
- build: 包生成脚本的路径
- links: 包链接到的本机库的名称
- exclude: 发布时要排除的文件。以数组形式给出,例如
exclude = ["/ci", "images/", ".*"]
- include: 发布时要包括的文件。以数组形式给出,例如
include = ["/src", "COPYRIGHT", "/examples", "!/examples/big_example"]
- publish: 可用于防止发布包
- metadata: 外部工具的额外设置
- default-run: 执行
cargo run
时默认要使用的 Crate - autobins: 禁用二进制自动发现
- autoexamples: 禁用示例自动发现
- autotests: 禁用测试自动发现
- autobenches: 禁用基准自动发现
- resolver: 设置要使用的依赖项解析器
- name: 名字,最少必须有该字段。如果是要发布到 crate.io 的包,则需要额外几个字段,例如 version 字段
- Target tables:
- [lib]: 库目标设置
- [[bin]]: 二进制目标设置
- [[example]]: 目标设置示例
- [[test]]: 测试目标设置
- [[bench]]: 基准目标设置
- Dependency tables:
- [dependencies]: 包库依赖项
- [dev-dependencies]: 示例、测试和基准的依赖项
- [build-dependencies]:构建脚本的依赖项
- [target]: 特定于平台的依赖项
- [badges]: 要在注册表中显示的徽章
- [features]: 条件编译特性
- [lints]: 配置 Lint
- [patch]: 重写依赖项。
- [replace]: 重写依赖项(已弃用)
- [profile]: 编译器设置和优化
- [workspace]: 工作区定义
-
版本号采用语义化版本(Semantic Versioning)(有时也称为 SemVer),这是一种定义版本号的标准。
-
Cargo 会根据这个文件中的
[dependencies]
从 registry 上获取所有包的最新版本信息放到$HOME/.cargo/
目录中
Cargo.lock
Cargo.lock 文件中包含了 Cargo.toml
中 [dependencies]
中给出的依赖的精确描述信息,它是一个确保任何人在任何时候重新构建代码,都会产生相同的结果一个实现方法。Cargo.lock 是由 Cargo 自动生成并维护,因此不要去手动修改。
当第一次构建项目时,Cargo 将根据 Cargo.toml 中用户给出的信息计算出所有符合要求的依赖版本并写入 Cargo.lock 文件。当将来构建项目时,Cargo 会发现 Cargo.lock 已存在并使用其中指定的版本,而不是再次计算所有的版本。
- 由于 Cargo.lock 文件对于可重复构建非常重要,因此它通常会和项目中的其余代码一样纳入到版本控制系统中。
- 当确实需要升级 crate 时,Cargo 提供了
cargo update
这个命令,它会忽略 Cargo.lock 文件,并计算出所有符合 Cargo.toml 声明的最新版本。
Crate
Crate 是 Rust 编译器一次处理的最小的单位,它既可以是一个库,也可以是一个可执行程序,分别被称为 Library Crate 和 Binary Crate。其中,Library Crate 的默认入口是 lib.rs
文件,他们不能被编译成可执行文件,也没有 main()
函数,而 Binary Crate 的默认入口则是 main.rs
文件的 main()
函数,它会被变为一个可执行程序。
- 使用
cargo new --bin pacakge_name
命令可以创建一个只包含src/main.rs
的 Package,其中只包含一个名字与 Package 相同 Crate,这个 Crate 就是一个 Binary Crate。其中--bin
可以省略,默认就是 Binary Crate。
- 使用
cargo new --lib pacakge_name
命令可以创建一个只包含src/lib.rs
的 Package,其中只包含一个名字与 Package 相同 Crate,这个 Crate 就是一个 Library Crate
- 使用
cargo install
命令可以从crate.io
上下载 Binary Crate 并在本地安装和使用。只有拥有二进制目标文件的包能够被安装,并会被安装到$HOME/.cargo/bin
中
crate root
crate root 是一个源码文件,Rust 编译器以它为入口点,自动查找并编译其中使用的其他源码文件。在 Cargo 中 Library Crate 的 crate root 固定为 src/lib.rs
,而 Binary Crate 的 crate root 则固定为 src/main.rs
。
crates.io
crates.io 是 Rust 官方提供的一个在线共享 crates 的平台,实际上就是 Rust 的一些 Package,就类似于一些前端语言的包共享平台(例如 Node.js 的 https://npmjs.org/)。这个平台就是用户通过 cargo
使用的包的来源,国外通常将类似平台称为 Package Registry
。
- crates.io 的全套源代码托管在了 https://github.com/rust-lang/crates.io 代码仓库中
发布自己的 crate
任何人都可以将自己编写的 crate 上传供其他人使用!首先需要在 https://crates.io 上注册账号(现在仅支持使用 Github 登陆),然后在 https://crates.io/settings/tokens 页面中创建一个 API token。
拿到 API token 之后,需要使用 cargo login 自己的 API token
命令登陆到 https://crates.io,以便进行后续的操作。(这个命令会告知 Cargo 你的 API token 并将其储存在本地的 ~/.cargo/credentials
文件中)
-
准备自己要共享的 Crate。在发布之前,确保
Cargo.toml
中的[package]
表中以下字段已经被设置[package] name = "guessing_game" version = "0.1.0" edition = "2021" authors = "ZCShou" categories = ["development-tools::procedural-macro-helpers", "parser-implementations"] description = "A fun game where you guess what number the computer has chosen." license = "MIT OR Apache-2.0" repository = "https://github.com/zcshou/xxx" documentation = "https://github.com/zcshou/" [dependencies]
name
、version
、edition
其中的name
需要一个唯一的名称,不能与 crates.io 上已有的重复license
指名使用的协议,协议名字参见 https://spdx.org/licenses/,多个时使用OR
间隔开,例如 Rust 自己使用的是license = "MIT OR Apache-2.0"
。或者使用license-file
指出自己的协议源文件,自己的协议源文件需要放到 Crate 中作为一部分description
描述自己的 Crate 的基本信息documentation
在线文档的网站。这就要求我们的 Crate 有比较友好的文档,文档的书写就是后文介绍的 rustdoc 工具生成的文档repository
源码仓库authors
作者categories
分类
-
使用
cargo package
命令对项目进行一些验证,然后将源代码压缩到 .crate 文件中 -
使用
cargo publish
就会自动发布到 crates.io 上。- 发布 crate 是上传特定版本的 crate 到 crates.io 以供他人使用,并且每个版本都不能被删除!
- 当修改了 crate 并准备好发布新版本时,改变 Cargo.toml 中 version 所指定的值,再次执行
cargo publish
即可 - 如果你要发布包到 crates.io 上,那该包的依赖也必须在 crates.io 上
- 虽然不能删除之前版本的 crate,但是支持通过
cargo yank --vers 1.0.1
来弃用,从而防止别人使用
在 crates.io 上分享的包可以是 Library Crate,也可以是 Binary Crate,但是无论哪种都是分享的源码。前者我们可以在自己的项目中的 [dependencies]
中来使用,而后者则可以通过 cargo install xxx
安装(下载源码编译成可执行程序)到本地(默认 $HOME/.cargo/bin
)来作为工具使用!
Module
Module(中文 模块
)是代码的组织单元,让我们可以将一个 crate 中的代码进行分组,以提高可读性与重用性。Module 还可以控制项的私有性,即其中的各项内容是可以被外部代码使用的(public),还是作为一个内部实现的内容,不能被外部代码使用(private)。
-
Module 可以嵌套
-
Module 的中的内容可以是函数,也可以是结构体、枚举、常量、trait 等
-
Module 和其他数据类型共享相同的命名空间,因此,模块名字不能和其他类型定义的名字重复
同文件中定义
Rust 规定可以在同一源码文件中使用 mod
关键字来定义一个 Module,格式为 mod module-name { }
。
独立源文件定义
可以将 Module 代码放到一个单独的源码文件中,而源文件名就是 Module 的名字。
子目录中定义
可以进一步将独立源文件放到一个子目录中,子目录中的每个独立源文件都是一个 Module。Rust 对这个子目录的格式有强制要求。
mod.rs
最初的 Rust(Rust 2015) 规定,如果将 Module 放到一个单独的子目录中的时候,就必须在子目录中定义一个 mod.rs
源码文件,否则,Rust 无法将子目录识别为一个 Module。而 mod.rs
中负责导出当前子目录的各个 Module。
-
子目录的名字就是 Module 的名字
-
mod.rs
中除了重导出其他 Module 外,还可以与普通源码文件一样,定义各种函数、结构体等等 -
子目录可以嵌套该结构,每一级目录中都添加一个
mod.rs
<folder_name>.rs
从 Rust 1.30 版本(Rust 2018)开始,Rust 新增通过在与 Module 子目录同级的目录中定义一个与子目录同名的 <folder_name>.rs
源文件来定义一个 Module 的方式。而 <folder_name>.rs
中就是原 mod.rs
的内容(导出子目录中的 Module)。
-
如果使用了
<folder_name>.rs
就不能在子目录下添加mod.rs
了,否则编译报错error[E0761]: file for module
utilsfound at both "src\utils.rs" and "src\utils\mod.rs"
-
子目录如果有多个,就需要多个
<folder_name>.rs
,与子目录一一对应 -
子目录可以嵌套该结构,每一级目录中都添加一个当面子目录同名的
<folder_name>.rs
-
可以混用
mod.rs
和<folder_name>.rs
这两种方式,不过不建议!
声明 Module
Rust 规定,要使用在独立源文件或者子目录中定义的 Module 时,必须先显式的在将 Module 导入到当前源文件。导入的方式就是使用 mod module_filename;
来声明。Module 必须从 main.rs/lib.rs
开始在模块树中逐级声明。
-
同文件中通过
mod module_name { }
定义的 Module 无需显式的声明 Module -
同级 Module(例如上图中的
hosting
和serving
)不能直接通过mod
互相声明,必须由它们的共同父模块(如上图的front_of_house.rs
)统一声明。如上图中front_of_house
又在main
中mod front_of_house
,从而形成main
➜mod front_of_house
➜hosting
和serving
-
如果模块是嵌套子目录则必须需要逐级声明。如上图中
check
先在front_of_house
中pub mod check;
,而front_of_house
又在main
中mod front_of_house
,从而形成main
➜mod front_of_house
➜check
➜foo
Module Tree
Rust 中实际上是使用 Module Tree 这种数据结构来组织所有 Module 之中的所有内容,Module 声明的规定也是为了符合这个 Module Tree 结构。而 Module 之间的互相使用就以此为基础。以上面图示的 front_of_house
为例对应的 Module Tree 如下所示:
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
一个单独的文件或文件夹就是一个独立的 Module,Module 的名字就是源码文件的名字或文件夹名字,被称为隐式模块。其中 src/main.rs
和 src/lib.rs
比较特殊,两个文件中的内容会构成名为 crate 的 Module,并作为 Module Tree 的根节点。这也是为什么 src/main.rs
和 src/lib.rs
称为 crate root 的原因。
-
如果一个模块 A 被包含在模块 B 中,我们将模块 A 称为模块 B 的子(child),模块 B 则是模块 A 的 父(parent)。例如上面的 front_of_house 与 hosting
-
处于同一级的 模块 A 和模块 B 则互为 兄弟(siblings),例如上面的 hosting 和 serving
-
所有模块都是名为 crate 的隐藏模块的子(child)模块
Module Path
Rust 中规定我们需要通过 Module Path 来索引使用 Module 的内容。对应于上面 Module Tree 这个数据结构,每一个要使用的 Module 及 Module 中的内容都是 Module Tree 的一个节点,每个节点就对应了一个唯一的路径,这个路径在 Rust 中就被称为 Module Path。
- 以
::
作为分隔符来间隔不同的节点
实际上,Module Path 这个概念就是一个类似 Linux 中文件系统的管理方式,Rust 中提供了如下三个路径关键字来消除路径歧义,并防止不必要的路径硬编码。
crate
:表示当前 Package 的根目录,相当于 Linux 文件系统中的/
super
:表示了当前位置的父级,相当于 Linux 文件系统中的..
self
:表示了当前位置,相当于 Linux 文件系统中的.
使用 Module 内容
使用 Module 内容必须显式通过上面 Module Path 这个结构来作为索引方法。第一种方法是可以选择从 crate
这个根节点开始访问,称为 绝对路径(absolute path) 法;第二种方法则是可以以 self
、super
或当前模块的标识符开始访问,称为 相对路径(relative path) 法。
- 要使用 Module 内容,则 Module 必须已经被显式的声明了
pub
关键字
Rust 中默认所有项(函数、方法、结构体、枚举、模块和常量)都是私有的。无论是绝对路径(absolute path) 法还是相对路径(relative path),都只能访问模块对外公开的程序项,而对程序项公开的方法就是在要公开的项前面添加 pub
关键字。
-
父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用它们父模块中的项。
-
同级的各个成员(例如兄弟模块之间)可以可以互相引用,而无需
pub
关键字 -
模块公有并不使其内容也是公有的。模块上的
pub
关键字只允许其父模块引用它,而不允许访问内部代码。当然仅公开模块通常没啥用,还需要同时我们根据需要公开一些模块中的内容 -
在一个结构体定义的前面使用了
pub
,这个结构体会变成公有的,但是这个结构体内的字段仍然是私有的 -
如果将枚举设为公有,则它的所有成员都将变为公有
super
关键字
super
可以用于在当前模块中访问父级模块中的内容,而无需 pub
关键字。但是,访问父级的子模块中的内容时,子模块中的内容本身必须为 pub
公开的。例如,嵌套的 Module 互相调用对的的函数时就可以使用 super
关键字。
self
则表示当前作用域,类似于 C++ 的this
指针
use
关键字
直接使用 Module 会导致我们代码里包含大量引用路径。使用 use
关键字可以将 Module 引入到当前作用域,use
后面的路径可以是绝对路径也可以是相对路径,从而简化对于 Module 中内容的使用的路径。
-
使用
use
引入函数时,习惯是导入到它的父级,而引入结构体、枚举和其他项时,习惯是指定它们的完整路径。- 例如上面示例中
use crate::front_of_house::hosting;
而不是use crate::front_of_house::hosting::add_to_waitlist;
- 如果上面示例中使用枚举时直接是
use crate::front_of_house::MealType;
- 例如上面示例中
-
use
关键字可以使用嵌套路径来消除大量的 use 行,格式use xx::yy::{zz, ss, ...};
。例如,use std::io::{self, Write};
就相当于use std::io;
+use std::io::Write;
-
通过 glob 运算符
*
将所有的公有定义引入作用域。例如,use std::collections::*;
将 std::collections 中定义的所有公有项引入当前作用域。 -
use
可以搭配as
来重定义模块的名字,例如use std::io::Result as IoResult;
-
当使用
use
关键字将一个名称导入当前作用域时,导入的名称只能在当前作用域使用(相当于当前作用的私有内容)。
重导出
重导出(Re-exports)允许我们有选择的公开模块中的内容,顺带还可以缩短 Module Path。对于一个稍微复杂点的库来说,通常需要有选择的导出其中的项目给其他调用者使用,重导出就可以实现该目的,实现方法就是使用 pub use
导出指定的内容。
假设有个名为 lib
的库,内容如下:
pub mod sub_module1 {
pub struct Foo;
}
pub mod sub_module2 {
pub struct AnotherFoo;
}
那么用户可以在自己的源码文件中导入库中的 Foo
和 AnotherFoo
这两个函数来使用
use lib::sub_module1::Foo;
use lib::sub_module2::AnotherFoo;
如果不希望将 sub_module1
和 sub_module2
暴露给用户,则可以添加重导出
// `sub_module1` and `sub_module2` are not visible outside.
mod sub_module1 {
pub struct Foo;
}
mod sub_module2 {
pub struct AnotherFoo;
}
// We re-export both types:
pub use crate::sub_module1::Foo;
pub use crate::sub_module2::AnotherFoo;
这样用户就可以这样 use lib::{Foo, AnotherFoo};
导入 Foo
和 AnotherFoo
这两个函数来使用
Workspace
Rust 中的一个工作空间是由多个 package 组成的集合,它们共享同一个 Cargo.lock 文件、输出目录和一些设置。组成工作空间的 packages 被称之为工作空间的成员。各成员的类型没有硬性要求,可以有任意多个 Binary Crate 或 Library Crate,也可以是示例(examples/)、测试(test/)、基准测试(benches/)。
-
Cargo 并不假设工作区中的各 Crate 会相互依赖,所以我们需要明确依赖关系
-
Workspace 中的所有的 Package 共同编译到根目录下的 target 目录中
-
工作区顶层只有一个
Cargo.lock
文件,而不是每个 Package 的目录中都有一个Cargo.lock
。当在不同的 Package 中包含了相同的依赖,则他们会被解析为一个放到Cargo.lock
中 -
即使 Workspace 中包含了某个依赖,不同的 Package 仍要在自己的
Cargo.toml
中进行显示声明为依赖 -
工作区至少必须有一个成员,要么是根包,要么是虚拟清单
-
注意,默认使用 Cargo 创建的 Package 都会有
.git
作为版本控制,根据自己的需要删除其中的.git
,一般一个 Workspace 用顶层一个版本控制即可
root package
若一个 Workspace 的 Cargo.toml
中包含了 [package]
的同时又包含了 [workspace]
部分,此时的 Workspace 本身也是一个 Package,它被称为 Root Package。一个工作空间的根( root )是该工作空间的 Cargo.toml
文件所在的目录。
-
此时的 Workspace 的目录结构必须符合上文介绍的 Package 的目录结构(必须有
src/main.rs
或src/lib.rs
) -
编译后,通常只会编译 Workspace 这个 Package 本身,其中的其他 Package 除非被 Workspace 这个 Package 依赖,否则不会编译
-
依赖必须是 Library Crate,如果有多个可执行程序的 Crate,需要编译时必须收到在
Cargo.toml
中用[[bin]]
指定,或者,可以使用cargo build --workspace
强制编译所有内容
virtual manifest
若一个 Workspace 的 Cargo.toml
中有 [workspace]
但是没有 [package]
部分,则它是虚拟清单(virtual manifest)类型的工作空间。对于没有主 Package 的场景或希望将所有的 package 组织在单独的目录中时,这种方式就非常适合。
-
就是把 workspace 当作组织多个 crate 的容器来使用
-
此时的 workspace 根目录必须有的只有
Cargo.toml
即可,无需符合上文的 Package 的目录结构! -
不允许在虚拟清单类型的工作空间中的
Cargo.toml
存在[dependencies]
字段,因为它不是一个 Package! -
编译后,根据其中 Package 类型的不同,会在根目录的 target 目录下生成不同的可执行文件
-
必须指定
resolver
因为它们没有package.edition
用来推断解析器版本。
[workspace]
Cargo 并没有直接提供一个命令来创建一个工作空间(workspace),而是需要我们手动在根目中创建一个 Cargo.toml
,然后在该 Cargo.toml
中添加一个 [workspace]
字段。
resolver
resolver 用于指定当前 Workspace 使用的依赖解析器版本,目前有 版本1,版本2 和 版本 3 这个三个可选。解析器是一个针对工作区的全局设置,该设置在依赖项中会被忽略。该设置仅对工作区的顶级包生效。如果使用的是虚拟工作区,必须显示在 [workspace]
定义中显式设置 resolver
字段,才能选择启用新的解析器。
- Edition 2024 依赖解析器默认是版本 3
- Edition 2021 依赖解析器默认是版本 2
- Edition 2015、Edition 2018 依赖解析器默认是版本 2
自 Rust 1.51.0 以来,Cargo 支持一种新的功能解析器,该解析器可以通过在 Cargo.toml
中设置 resolver = "x"
来启用。从 Rust 2021 开始,这将成为默认设置。也就是说,在 Cargo.toml
中写入 edition = "2021"
将自动包含 resolver = "2"
。
members / exclude
members 和 exclude 字段共同定义哪些包是工作区的成员。其中,members 指定了要包含在工作区中的包,exclude 指定了要从工作区中排除的包。
- 后续我们通过
cargo new xxx
时会自动在Cargo.toml
中添加到 members 中
[dependencies]
单独的 [dependencies]
是用于指定 Package 的依赖的,对于 Workspace,需要通过 [workspace.dependencies]
来定义当前 Workspace 中的所有成员可以共享的依赖!
[package]
单独的 [package]
是用于指定 Package 的基本信息的,对于 Workspace,需要通过 [workspace.package]
来定义当前 Workspace 中的所有成员可以共享的 package 信息!
[lints]
单独的 [lints]
是用于指定 Package 的 lints 信息,对于 Workspace,需要通过 [workspace.lints]
来定义当前 Workspace 中的所有成员可以共享的 lints 信息!
其他
- default-members — 当没有选择特定的包时要操作的包
- metadata — Extra settings for external tools.
[patch]
/ [replace]
[patch]
和[replace]
指定了覆盖依赖项,不过[replace]
已弃用
[profile]
[profile]
用来进行编译器设置和优化
使用第三方包
Rust 编程可以使用第三方的包,很多人将常用的一些功能编写为了一个库供其他人使用,Rust 官方的共享平台是 crates.io
。因此,当我们编写 Rust 代码时,可以先在 crates.io
查找是否有可用的第三方库来简化我们的实现。当然,第三包的分享平台并不局限于 crates.io,很多包在 Github 上提供源码!
Rust 的项目管理工具 Cargo 同时还是一个软件包的管理工具,当我们在自己的项目中使用了其他软件包时,Cargo 就会自动帮助我们下载我们依赖的软件包,并自动帮我们编译到自己的项目中!
[dependencies]
要使用第三方包,需要在自己的项目的 Cargo.toml
文件中的 [dependency]
下列出依赖的包。Rust 支持多种方式来引入第三方包,具体如下:
-
直接以
包名 = 版本
手动列出依赖包,则 Cargo 默认会从crates.io
(可以通过 Cargo 的配置文件更改包的源地址)下载我们的依赖包。- 还可以使用
cargo add 名字@版本
来增加包 - 使用
cargo update 名字@版本
更新某个包
- 还可以使用
-
可以直接引入 git 仓库作为依赖包。可以使用
rev = "HASH 值"
、tag = " TAG 名"
或branch = "分支名"
来指定想要拉取的版本。如果不指定版本,Cargo 会假定我们使用 master 或 main 分支的最新 commit[dependencies] regex = { git = "https://github.com/rust-lang/regex", branch = "next" }
-
通过路径引入本地依赖包
[dependencies] hello_utils = { path = "hello_utils" } # 以下路径也可以 # hello_utils = { path = "./hello_utils" } # hello_utils = { path = "../hello_world/hello_utils" }
-
可以根据特定的平台来引入依赖,语法跟 Rust 的
#[cfg]
语法非常相像,因此还能使用逻辑操作符进行控制[target.'cfg(windows)'.dependencies] winhttp = "0.4.0" [target.'cfg(unix)'.dependencies] openssl = "1.0.1" [target.'cfg(target_arch = "x86")'.dependencies] native = { path = "native/i686" } [target.'cfg(target_arch = "x86_64")'.dependencies] native = { path = "native/x86_64" } [target.'cfg(not(unix))'.dependencies] openssl = "1.0.1"
[dev-dependencies]
对于只在测试时需要的依赖库需要添加到 Cargo.toml
文件中的 [dev-dependencies]
分组下面,其中的依赖只会在运行测试、示例和 benchmark 时才会被引入。当然,还可以指定平台特定的测试依赖包。
[dev-dependencies]
tempdir = "0.3"
[target.'cfg(unix)'.dev-dependencies]
mio = "0.0.1"
[build-dependencies]
构建脚本 build.rs
使用的依赖需要放到 Cargo.toml
文件中的 [build-dependencies]
分组下面,同样,还可以指定平台特定的测试依赖包。构建脚本( build.rs )和项目的正常代码是彼此独立,因此它们的依赖不能互通。
[build-dependencies]
cc = "1.0.3"
[target.'cfg(unix)'.build-dependencies]
cc = "1.0.3"
版本兼容
Rust 本身以及编程中使用的其他软件包,都涉及到不同版本兼容的问题。前面说了,Rust 使用语义化版本号(semver)格式的版本,格式为 x.y.z
的形式,其中 x 被称为主版本号 major,y 被称为次版本 minor,而 z 被称为补丁号 patch。可以看出从左到右,版本的影响范围逐步降低,Rust 的版本兼容策略就是以此为基础。
- Cargo 认为
0.0.z
是非常原始的版本和其它任何版本都不兼容 - Cargo 认为
0.y.z
(y 非零) 的版本只能与相同y
值的版本兼容,例如,0.6.3
和0.6.1
兼容,但是0.7.1
和0.6.1
不兼容 - Cargo 认为
1.y.z
及之后的版本与所有主版本号相同的版本兼容,例如,2.17.99
和2.0.1
兼容,但是3.0.1
和2.0.1
不兼容
直接版本号
当我们要指定包的版本号时,通常是直接给出具体的版本号,例如,rand = "0.8.5"
,然而,Cargo 却不一定会使用我们指定的版本,而是自动尝试使用它认为的与之兼容的最新的版本。我们可以通过添加一些特殊符号来进行限定!
^
指定版本
除了直接给出版本号,还可以在版本号之前添加 ^
来指定一个版本号范围,然后 Cargo 会使用该范围内的最大版本号来引用对应的包。 实际上我们直接写版本号就等同于用 ^
指定版本(因为 ^
是可以省略的),例如,rand = "1.2.3"
就等同于 rand = "^1.2.3"
。
^1.2.3 等价于 >=1.2.3, <2.0.0
^1.2 等价于 >=1.2.0, <2.0.0
^1 等价于 >=1.0.0, <2.0.0
^0.2.3 等价于 >=0.2.3, <0.3.0
^0.2 等价于 >=0.2.0, <0.3.0
^0.0.3 等价于 >=0.0.3, <0.0.4
^0.0 等价于 >=0.0.0, <0.1.0
^0 等价于 >=0.0.0, <1.0.0
~
指定版本
~
指定了兼容版本的最小化版本,根据指定的主版本号、次版本号、补丁号的不同,限制的范围不同。
~1.2.3 等价于 >=1.2.3, <1.3.0 指定主要版本、次要版本和补丁版本,仅允许进行补丁级别的兼容
~1.2 等价于 >=1.2.0, <1.3.0 仅指定主要版本和次要版本,仅允许进行补丁级别的兼容
~1 等价于 >=1.0.0, <2.0.0 仅指定主要版本,则允许进行次要和补丁级别
*
通配符
使用 *
这种方式允许将 *
所在的位置替换成任何数字。
* 等价于 >=0.0.0 注意 crates.io 不允许裸 * 版本
1.* 等价于 >=1.0.0, <2.0.0
1.2.* 等价于 >=1.2.0, <1.3.0
比较符
可以使用比较符的方式来指定一个版本号范围或一个精确的版本号,同时还能使用比较符进行组合,并通过逗号分隔:
>= 1.2.0
> 1
< 2
= 1.2.3
>= 1.2, < 1.5
文档规范
文档一直都是一个项目最为关键但是很麻烦的工作。Rust 官方专门为文档的编写制定了相关规范,并以此编写了 Rust 相关的各种文档。为此,Rust 官方还提供了一个名为 mdBook
的文档处理工具,以方便编写独立文档,同时还提供了一个名为 rustdoc
的文档处理工具,用于将源码中的注释直接生成文档。
mdBook
mdBook 是一个将一系列 Markdown 文档(.md
)创建为一套 HTML 文档的命令行工具。它是创建产品或 API 文档、教程、课程材料或任何需要简洁、易于导航和可定制的演示文稿的理想工具。Rust 提供的绝大多数文档都是使用 mdBook 来构建的,汇总如下:
关于 mdBook 的详细介绍,参见博文 Rust 之一 基本环境搭建、各组件工具的文档、源码、配置
rustdoc
rustdoc 是 Rust 官方的文档处理工具,并附带 Rust 编译工具链发布,这个工具主要是用来自动收集当前项目源码文件中的各种注释(有特殊格式要求,见下文)来生成对应文档,
它接受一个 crate root 文件(也支持一个 markdown 文件)作为参数,然后生成 HTML,CSS 和 JavaScript 文件等组成一套静态网页文档。如下官方文档就是使用 rustdoc 生成的:
名称 | 在线地址 | 源码地址 |
---|---|---|
The Rust Standard Library | https://doc.rust-lang.org/std/index.html | <> |
The Rust Standard Library | https://doc.rust-lang.org/core/index.html | <> |
The Rust Standard Library | https://doc.rust-lang.org/core/index.html | <> |
基本格式
rustdoc
会提取源码文件中特定格式的注释(官方称为文档注释)来生成文档,rustdoc
规定了用于生成的文档的注释必须以 ///
或者 //!
开头,并且注释的内容主要使用 Markdown 语法来进行编写。
///
用于放在函数、结构体、变量等代码前面以对代码进行注释,称为 outer documentation,示例如下:
//!
用于放到一个源码文件的开头给一个源码文件注释,称为 inner documentation。示例如下:
- 当它位于 root crate 文件时,那么它就是文档主页的注释
- 其最后面需要加一个空行
- 可以在自己的
src/lib.rs
或main.rs
的开头添加#![warn(missing_docs)]
,而如果是一个要共享的库则可添加#![deny(missing_docs)]
- 使用
#[doc(hidden)]
可以屏蔽将之后的内容提取到文档中
编码规范
代码的命名规范、风格、注释方式一直都是程序员比较头疼的一个问题,因为不同的人总有自己的想法和风格。Rust 官方则对 Rust 编程制定了一系列的编码规范,并提供了 rustfmt
这个代码格式化工具来帮助程序员处理这些问题。Rust 语言社区内其实分散着很多编码规范:
- 官方|Rust API 编写指南
- 官方 | Rust Style Guide
- Rust’s Unsafe Code Guidelines Reference
- 法国国家信息安全局 | Rust 安全(Security)规范
- Apache Teaclave 安全计算平台 | Rust 开发规范
- PingCAP | 编码风格指南(包括 Rust 和 Go 等)
- Google Fuchsia 操作系统 Rust 开发指南
- RustAnalyzer 编码风格指南
- 使用 Rust 设计优雅的 API
- Rust FFI 指南
命名规范
Rust 倾向于在“类型”级的结构中使用大驼峰( UpperCamelCase) 命名风格,在 “变量、值(实例)、函数名”等结构中使用蛇形( snake_case)命名风格。针对不同的类型,Rust 官方建议的命名方式如下表所示:
类型 | 命名规则 | 示例 |
---|---|---|
Crates / Package | snake_case | cook.rs make_bin.rs |
Modules | snake_case | Mod cook { … } |
Types | UpperCamelCase | |
Traits | UpperCamelCase | |
Enum variants | UpperCamelCase | enum WebEvent { PageLoad, PageUnload, KeyPress(char), Paste(String), Click { x: i64, y: i64 }, } |
Functions | snake_case | fn calc_crc() { … } |
Methods | snake_case | |
General constructors | new or with_more_details | |
Conversion constructors | from_some_other_type | |
Macros | snake_case! | macro_rules! macro_name { // 宏规则 ($arg1:pat, $arg2:expr) => { // 生成的代码 }; } 使用时 macro_name!() 来使用 |
Local variables(包括函数的形参和实参) | snake_case | let first_name = “zcs” |
Statics | SCREAMING_SNAKE_CASE | static FIRST_NAME : &str = “zcs”; |
Constants | SCREAMING_SNAKE_CASE | const FIRST_NAME : &str = “zcs” |
Type parameters | concise UpperCamelCase, usually single uppercase letter: T | |
Lifetimes | short lowercase, usually a single letter: 'a, 'de, 'src | |
Features | snake_case |
-
通常使用
动词-宾语[-error]
这种结构。例如,标准库中有JoinPathsError
、ParseBoolError
等 -
在 UpperCamelCase 情况下,由首字母缩写组成的缩略语和复合词的缩写,应算作单个词。比如,应该使用 Uuid 而非 UUID,使用 Usize 而不是 USize,或者是 Stdin 而不是 StdIn。
-
在 snake_case 中,首字母缩写和缩略词是小写的 is_xid_start。
-
在 snake_case 或者 SCREAMING_SNAKE_CASE 情况下,每个词不应该由单个字母组成,除非这个字母是最后一个词。比如,使用 btree_map 而不使用 b_tree_map,使用 PI_2 而不使用 PI2 。
-
由于历史问题,包名有两种形式 snake_case 或 kebab-case ,但实际在代码中需要引入包名的时候,Rust 只能识别 snake_case,也会自动将 kebab-case 识别为 kebab_case。所以建议使用 snake_case。
-
类型转换要遵守 as_,to_,into_ 命名惯例。类型转换应该通过方法调用的方式实现,其中的前缀规则如下:
方法前缀 性能开销 所有权改变 示例 as_ Free borrowed -> borrowed str::as_bytes() 把 str 变成 UTF-8 字节数组,性能开销是 0。输入是一个借用的 &str,输出也是一个借用的 &str to_ Expensive borrowed -> borrowed
borrowed -> owned (non-Copy types)
owned -> owned (Copy types)Path::to_str 会执行一次昂贵的 UTF-8 字节数组检查,输入和输出都是借用的。对于这种情况,如果把方法命名为 as_str 是不正确的,因为这个方法的开销还挺大 into_ Variable owned -> owned (non-Copy types) String::into_bytes() 返回 String 底层的 Vec<u8>
数组,转换本身是零消耗的。该方法获取 String 的所有权,然后返回一个新的有独立所有权的Vec<u8>
- 当一个单独的值被某个类型所包装时,访问该类型的内部值应通过 into_inner() 方法来访问。
- 如果 mut 限定符在返回类型中出现,那么在命名上也应该体现出来。例如,Vec::as_mut_slice 就说明它返回了一个 mut 切片
-
读访问器(Getter)的名称遵循 Rust 的命名规范
-
一个集合上的方法,如果返回迭代器,需遵循命名规则:iter,iter_mut,into_iter
fn iter(&self) -> Iter // Iter implements Iterator<Item = &U> fn iter_mut(&mut self) -> IterMut // IterMut implements Iterator<Item = &mut U> fn into_iter(self) -> IntoIter // IntoIter implements Iterator<Item = U>
-
迭代器的类型应该与产生它的方法名相匹配。例如形如 into_iter() 的方法应该返回一个 IntoIter 类型
代码风格
制定统一的编码风格可以大大提升代码的可读性,让日常代码维护和团队之间审查代码更加方便。Rust 官方定义了 Rust 代码的整体风格,并且提供了自动化格式化工具 rustfmt
来帮助我们处理代码风格。
-
每级缩进必须为 4 个空格,而不是制表符(
TAB
)进行代码对齐 -
一行的最大宽度为 100 个字符,但是完全是注释的行的长度应限制为 80 个字符
-
优先使用块缩进而不是视觉缩进
// Block indent a_function_call( foo, bar, ); // Visual indent a_function_call(foo, bar);
-
在任何类型的逗号分隔列表中,在后跟换行符时使用尾随逗号
-
用零或一个空行分隔项目和语句。
-
函数定义,注意各个关键字的顺序,以及
{}
的位置[pub] [unsafe] [extern ["ABI"]] fn foo(arg1: i32, arg2: i32) -> i32 { ... }
-
更详细的格式参见 https://doc.rust-lang.org/nightly/style-guide/
注释风格
在 Rust 中,注释可以分为普通注释和文档注释两类。普通注释使用 //
或 /* ... */
,文档注释使用 ///
(等同于把注释内容写入 #[doc="..."]
里)、//!
(等同于把注释内容写入 #![doc=“…”] 里)。注意,注释需要放到要注释的内容的前面。
-
//
是单行注释,注释内容直到行尾
- 可以嵌套,但是一般没有这么用的
- 不能放到代码语句内部,只能放到代码结尾
- 优先选择行注释 (
//
) 而不是块注释 (/* ... */
)
-
/* ... */
为块注释,注释内容在/*
和*/
之间可以任意多行
- 块注释可以嵌套,但是一般没有这么用的
- 多行块注释通常在每行前面加一个
*
是常用的风格,但不是必须得 - 块注释可以插入到代码语句内部
-
///
也是单行注释,也有对应的块注释格式为/** ... */
,用于放在函数、结构体、变量等代码前面以对代码进行注释,称为 outer documentation,///
开头的注释会被rustdoc
提取用于生成文档,示例如下:
- 注释的内容主要使用 Markdown 语法来编写
- 优先选择行注释 (
///
) 而不是块注释 (/** ... */
)
-
//!
单行注释,也有对应的块注释格式为/*! ... */
,用于放到一个源码文件的开头给一个源码文件注释,称为 inner documentation。//!
开头的注释会被rustdoc
提取用于生成文档,示例如下:
- 注释的内容主要使用 Markdown 语法来编写
- 当它位于 root crate 文件中时,那么它就是文档主页的注释
- 其最后面需要加一个空行
-
孤立的 CRs(
\r
),如果其后没有紧跟有 LF(\n
),则不能出现在文档型注释中
参考
- https://medium.com/@gftea/rust-language-notes-part-2-7f0b47168029
- https://users.rust-lang.org/t/cargo-package-structure-cheat-sheet/47984
- https://toml.io/cn/
- https://www.andy-pearce.com/blog/posts/2023/May/uncovering-rust-build-and-packaging/
- https://cloud.tencent.com/developer/article/2415749
- https://course.rs/cargo/reference/workspaces.html
- https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/