这是Rust主题下的小小理念之一: 系统程序员也能享受美好. ---- Robert O'Callahan
本章介绍的特型:
- crate: 项目间代码共享
- 模块: 项目内代码组织
- 版本管理
- 记录和测试Rust代码
- 消除不必要编译器警告
- 发布库
8.1 crate (板条箱)
- Rust程序由crate(板条箱)构成
- crate是完整且内聚的单元: 包括库, 可执行程序的所有源码, 测试, 工具, 其他杂项

crate工作过程:
Cargo.toml
: 需要的crate及其版本[dependencies]
: 项目所依赖的外部库, cargo编译时自动下载这些依赖 (在开源站点crates.io上检索)- 依赖图: 如果依赖库下仍有依赖, 构成了crate的依赖图, 而Cargo可以自动处理
- 编译crate: 下载完成所有依赖后, Cargo会编译所有crate (为每个依赖运行一次rustc)
- Rust将代码静态链接到最终可执行文件
- 其他:
cargo build --release
会忽略断言, 生成优化的程序, 运行更快, 代价是编译时间更长
[package]
name = "actix-gcd"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
8.1.1 版本
为了避免版本问题 (最常见的: 新增标识符导致旧版程序无法编译), Rust提供了版本控制.
Rust的版本控制只针对语法解释, 2015下的crate仍然可以与2021下的crate联动.
[package]
name = "actix-gcd"
version = "0.1.0"
edition = "2021" #版本控制
8.1.2 创建配置文件
Cargo.toml可以设定不同区段
命令行 | 使用的Cargo.toml区段 |
---|---|
cargo build | [profile.dev] |
cargo build --release | [profile.release] |
cargo test | [profile.test] |
8.2 模块
- 模块功能: 项目内代码组织, 管理命名空间
- 模块定义: 一组语法项的集合, 且语法项具有命名的特型
- 语法项定义: 模块, 函数, 结构体, 枚举, 常量等命名实体
pub
: 令某个语法项为公共项, 可以从模块外部访问pub(crate)
: 令某个语法项在crate中任意使用(父级, 同级模块也能使用), 但不会作为外部结构公开.私有
: 默认私有, 只能在定义它的模块及其子模块中可见 (重点: 子模块可见)pub(in <path>)
: 在特定路径下(及其子模块)可见
// 孢子模块
mod spores {
use cells::{Cell, Gene};
pub struct Spore { // 孢子结构体
...
}
// 模拟孢子分裂
pub fn produce_spore(factory: &mut Sporangium) -> Spore {
...
}
// 提取基因
pub(crate) fn genes(spore: &Spore) -> Vec<Gene> {
...
}
}
8.2.1 嵌套模块
特型:
可见度问题:
私有mod
可以包含pub mod
- 但是, 如果从外部访问一个
pub mod
, 而其父mod
是私有
的话, 无法访问. (必须保证路径所有的mod
都是pub mod
)
更优解: 拆分源文件, 利用文件树来辅助划分
mod plant {
pub mod roots { // plant的子模块
...
}
pub mod stems{ // plant的子模块
...
}
}
8.2.2 单独文件中的模块
文件树
src/
├── main.rs # 入口文件
└── spores.rs # `spores` 模块
main.rs
mod spores; // 声明 `spores` 模块,Rust 会查找 `spores.rs`
fn main() {
spores::grow(); // 调用 `spores.rs` 中的 `grow` 函数
}
spores.rs: 无需mod
关键字来声明自己是一个模块, Rust将spores.rs
整体视为mod spores
模块
pub fn grow() {
println!("Spores are growing!");
}
更一般的文件树
表示目录模块的方式:
- 目录下创
mod.rs
文件 - 同级目录创建
plant
目录和plants.rs
文件
fern_sim/
├── Cargo.toml
└── src/
├── main.rs
├── spores.rs spores模块
└── plant/
├── mod.rs plant目录下的mod.rs表示plant模块
├── leaves.rs 子模块
├── roots.rs 子模块
└── stems.rs 子模块
8.2.3 路径与导入
::
用于访问模块中的语法项
use std::mem;
use std::collections::{HashMap, HashSet}; // 同时导入两个模块
use std::io::Result as IOResult; // 导入, 同时赋予本地别名
if s1 > s2 {
std::mem::swap(&mut s1, &mut s2); // 直接访问
mem::swap(&mut s1, &mut s2); // 导入访问 (导入模块而不是具体的语法项, 通常是更好的选择)
}
父子模块的导入 (super关键字)
父模块proteins/mod.rs
pub enum AminoAcid { ... }
pub mod synthesis; // 声明子模块
子模块proteins/synthesis.rs
use super::AminoAcid; // super表示父模块, self表示自身模块, crate表示当前模块所在的crate (相当于src)
pub fn synthesize(seq: &[AminoAcid]) { // 错误: 即使AminoAcid是pub, 仍然需要导入后才可见
...
}
crate与模块同名情况
假设: 存在一个image crate
和一个image模块
use image::Pixels; // 存在歧义
// 解决方法
use ::image::Pixels; // 表示image crate
use self::image::Pixels; // 表示image模块
8.2.4 标准库预导入
rust会将常用的标准库内容预导入 (底层自动导入, 比如: Vec和Result都可以直接使用)
use std::prelude::v1::*; // 底层自动包含该use语句
8.2.5 公开use声明
use
语法的本质: 创建一个别名, 在当前作用域引入某个item, 从而方便访问.pub use
: 导入当前模块时, 也可以通过当前路径访问pub use
导入的内容
plant.rs
...
pub use self::leaves::Leaf; // 导入到当前模块中
main.rs
use plant;
fn main() {
plant::take_leaf(); // 因为pub use, plant可以直接使用Leaf中的item
}
8.2.6 公开结构体字段
- 私有字段: 模块及其子模块可以访问
- 公共字段: 外部访问
Rust结构体与类区别:
- Rust结构体: 私有字段同模块共享
- 类: 私有字段无法共享
- 区别: 同模块共享可以更加灵活, 同模块下可以定义多个紧密协作的类型, 互相访问私有字段
pub struct Fern {
pub roots: RootSet,
pub stems: StemSet,
}
8.2.7 静态变量与常量
- 静态变量(默认不可变, 可以显式声明mut): 生命周期持续整个进程的变量
- 使用场景: 常量值引用
- 其他说明: Rust不鼓励mut静态变量, 因为线程不安全
- 常量: 类似于C语言中的
#define
宏定义- 使用场景: 魔数, 字符串
pub const ROOM: f64 = 20.0; // 常量: 室内温度
pub static OUTDOOR: f64 = 8.0; // 静态变量: 室外温度
8.3 库创建
- 编写库中的语法项 (需要外部访问的需要
pub
) - 修改
main.rs
为lib.rs
- 无需修改
Cargo.toml
文件, Rust自动检查文件名, 如果为lib.rs
则编译为库
注意: 示例中结构体(字段), 方法, 函数均为pub
, 可以外部访问
// 植物结构体
pub struct Fern {
pub size: f64,
pub growth_rate: f64
}
// 植物的方法
impl Fern {
// 生长方法
pub fn grow(&mut self) {
self.size *= 1.0 + self.growth_rate;
}
}
// 模拟生长函数
pub fn run_simulation(fern: &mut Fern, days: usize) {
for _ in 0 .. days {
fern.grow();
}
}
8.4 src/bin目录
crate中: 程序和库可以同时存在.
创建src/bin/efern.rs
文件, 并写入main
函数
使用cargo run
即可正常编译, 分别编译:
- 编译库
- 编译可执行程序
use example::{Fern, run_simulation}; // example由Cargo.toml中的name字段决定
fn main() {
let mut fern = Fern {
size: 1.0,
growth_rate: 0.001
};
run_simulation(&mut fern, 1000);
println!("final fern size: {}", fern.size);
}
甚至可以在bin下创建其他的子模块, Rust自动将bin中的rs文件视为构建的额外程序
fern_sim/
├── Cargo.toml
└── src/
└── bin/
├── efern.rs 模块
└── draw_fern/
├── main.rs 可执行程序
└── draw.rs 模块
如何导入自创库
[dependencies]
fern_sim = { path = "../fern_sim" }
8.5 属性
Rust中任何语法项(item)都可以用属性修饰.
属性定义: Rust通用语法, 用于向编译器提供各种指令和建议
#[allow]
: 允许禁用警告, 不显示
#[cfg]
: 条件编译属性, 根据编译时配置有选择编译代码
#[cfg(...)] 选项 | 当启用时······ |
---|---|
test | 启用测试(使用 cargo test 或 rustc --test 编译) |
debug_assertions | 启用调试断言(通常在非优化构建中) |
unix | 为 Unix(包括 macOS)编译 |
windows | 为 Windows 编译 |
target_pointer_width = "64" | 针对 64 位平台。另一个可能的值是 "32" |
target_arch = "x86_64" | 特别针对 x86-64。其他值有:"x86" , "arm" , "aarch64" , "powerpc" , "powerpc64" , "mips" |
target_os = "macos" | 为 macOS 编译。其他值有:"windows" , "ios" , "android" , "linux" , "freebsd" , "openbsd" , "netbsd" , "dragonfly" |
feature = "robots" | 启用名为 "robots" 的用户自定义特性(用 cargo build --feature robots 或 rustc --cfg feature="robots" 编译)。这些特性是在 Cargo.toml 的 [features] 区段中声明的。 |
not(A) | 不满足条件 A 时。如果要提供某函数的两种实现,请将其中一个标记为 #[cfg(X)] ,另一个标记为 #[cfg(not(X))] |
all(A, B) | 同时满足 A 和 B (相当于 && ) |
any(A, B) | 只要满足 A 或 B 之一(相当于 ` |
#[inline]
: 指定函数内联 (编译优化技术, 在调用点直接展开函数代码)
- 属性附加在封闭区域:
#!
替换#
- 属性附加在整个crate:
#!
替换#
, 并写在main.rs
或者lib.rs
顶部
8.6 测试与文档
- 测试函数定义: 测试函数带有
#[test]
, 使用cargo test
运行所有测试 - 测试内容: 一般使用
assert!
和assert_eq!
测试- 可以使用
debug_assert!
和debug_assert_eq!
尽在调试构建中检查的断言
- 可以使用
- 测试函数的性能: Rust测试工具使用多个线程运行多个测试
#[test]
fn math_works() {
let s: i32 = 1;
assert!(s.is_positive());
assert_eq!(x + 1, 2);
}
8.6.1 集成测试
- 集成测试: 在src目录旁边创建tests目录, 内部创建rs文件
- 测试:
cargo test
- 优势: 集成测试编译为独立crate, 模拟外部以crate调用该lib的过程, 更加仿真
8.6.2 文档
# 创建HTML文档
# --no-deps: 仅为fern_sim生成文档, 不会对依赖项生成
# --open: Cargo随后在浏览器中打开此文档
$ cargo doc --no-deps --open
- 文档根据库中pub特型以及附加的所有文档型注释生成的.
- 文档注释:
#[doc = "注释"]
///
, Rust看到三斜杠视为#[doc]
属性
8.6.3 文档测试
- 运行
cargo test
, Rust自动为文档中出现的代码块生成测试代码(主要是断言), 并进行测试. - 如果希望只编译不运行, 请在代码块类型中添加
no_run
- 如果连编译都不希望, 请在代码块类型中添加
ignore
8.7 指定依赖项
如果代码没有发布在crates.io上, 可以通过指定Git库URL和修订号
image = { git = "https://github.com/Piston/image.git", rev = "528f19c" }
// 本地crate引用
image = { path = "vendor/image" }
8.7.1 版本
指定版本, Rust解析标准十分宽松: 选择与指定版本兼容的最新版本
image = "0.6.1" # Rust自动选择兼容版本好
image = "=0.10.0" # 指定确定的版本
image = ">=1.0.5" # 使用1.0.5及其更高版本
image = ">1.0.5<1.1.9" # 1.0.5到1.1.9之间的版本
image = "<=2.7.10" # 2.7.10及其更低版本
8.7.2 Cargo.lock
- Cargo.toml中的版本号是故意保持灵活的, 但是编译时通常希望固定的版本号.
- Cargo.lock在第一次构建时生成, 用来记录crate的确切版本, 以后的构建都将参考lock使用相同版本.
- 更新版本
- 直接修改Cargo.toml
cargo update
: 只会升级到兼容版本
8.8 将crate发布到crates.io
略
8.9 工作空间
问题: 如果三个crate使用了重叠的依赖项, 独立编译会导致包含重复依赖导致磁盘浪费
解决方法: 在fernsoft下创建Cargo.toml
文件
[workspace]
mambers = ["fern_sim", "fern_img", "fern_video"]
删除所有子crate中的Cargo.lock
和target
目录
完成后所有的cargo build均会在根目录下创建和使用共享的target
目录
fernsoft/
├── .git/...
├── fern_sim/
│ ├── Cargo.toml
│ ├── Cargo.lock
│ ├── src/...
│ └── target/...
├── fern_img/
│ ├── Cargo.toml
│ ├── Cargo.lock
│ ├── src/...
│ └── target/...
└── fern_video/
├── Cargo.toml
├── Cargo.lock
├── src/...
└── target/...