08. Crates & Modules — 包与模块
程序到一定规模后,把所有代码放在一个文件里是行不通的。Rust 提供了两级代码组织机制:Crate(包,编译和分发的基本单元)和模块(模块,crate 内部的命名空间组织)。同时,可见性规则控制哪些代码可以访问哪些项目。
理解这两级结构是编写真实的 Rust 程序的前提——从命令行工具到大型库都依赖它们。
Crate:编译的基本单元
Crate 是 Rust 的编译单元。rustc 一次编译一个 crate,产生一个可执行文件或一个库。Cargo 在幕后调用 rustc 时,为你管理依赖和编译顺序。
两种 Crate
| 类型 | 入口文件 | 产物 |
|---|---|---|
| 二进制(binary) | src/main.rs | 可执行文件 |
| 库(library) | src/lib.rs | .rlib 文件 |
一个项目可以同时包含一个库 crate 和多个二进制 crate。典型布局:
my_project/
├── Cargo.toml
├── src/
│ ├── lib.rs # 库 crate 的根
│ └── main.rs # 默认二进制 crate
├── src/bin/
│ ├── tool1.rs # 额外的二进制 crate
│ └── tool2.rs # 另一个二进制 crate
├── tests/ # 集成测试(每个文件是一个独立的 crate)
└── examples/ # 示例程序Cargo.toml
Cargo 的清单文件定义 crate 的元数据和依赖:
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
description = "A useful tool"
license = "MIT"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = "1.35"
rand = "0.8"Cargo 会递归解析所有传递依赖,构建一个完整的依赖图。Cargo.lock 记录了构建中每个 crate 的精确版本——保证可重现构建。
Edition(版次)
Rust 通过 Edition 机制实现向后不兼容的语言变更,同时保持跨 Edition 的 crate 兼容性。关键 Edition:
| Edition | 年份 | 关键变化 |
|---|---|---|
| 2015 | 2015 | 初始版本 |
| 2018 | 2018 | async/await 关键字,模块路径改革,dyn Trait |
| 2021 | 2021 | 闭包捕获优化,IntoIterator for arrays |
不同 Edition 的 crate 可以无缝互操作——Edition 是每个 crate 的独立选择。用 cargo fix 可以自动迁移代码到新 Edition。
Build Profile
Cargo 提供标准的构建配置:
[profile.dev] # cargo build(默认)
opt-level = 0 # 不优化,快速编译
debug = true # 包含调试信息
[profile.release] # cargo build --release
opt-level = 3 # 激进优化
debug = false # 去掉调试信息
lto = true # 链接时优化可以在 Cargo.toml 中覆盖这些设置:
[profile.release]
debug = true # release build 也包含调试信息模块
模块是 Rust 的命名空间机制——控制项目(函数、类型、常量等)的组织和可见性。
mod 定义模块
mod spores {
// 默认私有的函数
fn recombine(parent: &SporeCell) -> SporeCell {
// ...
}
// pub 导出整个 crate 可见
pub struct SporeCell {
pub density: f64, // 字段公开
temperature: f64, // 字段私有(仅模块内可访问)
}
// pub(crate) 导出仅 crate 内部可见
pub(crate) fn genes() -> Vec<String> {
// ...
}
// pub 函数可以在 crate 外使用
pub fn produce_spore(cell: &SporeCell) -> Spore {
// 内部可以访问私有函数和字段
let genes = recombine(cell);
Spore { /* ... */ }
}
}可见性修饰符
| 修饰符 | 可见范围 |
|---|---|
| (默认,不写) | 仅当前模块及其子模块 |
pub | 任何地方 |
pub(crate) | 当前 crate 内任何地方 |
pub(super) | 父模块 |
pub(in path) | 指定路径的模块内 |
pub(in path) 用于非常精确的控制:
mod plant_structures {
pub mod roots {
pub(in crate::plant_structures) struct Cytokinin {
// 仅 plant_structures 模块内部可访问
}
}
}模块可以嵌套:
mod plant {
pub mod roots {
pub fn absorb_water() { /* ... */ }
}
pub mod stems {
pub fn transport() { /* ... */ }
}
pub mod leaves {
pub fn photosynthesize() { /* ... */ }
}
}
// 使用嵌套模块中的项目
plant::roots::absorb_water();
plant::leaves::photosynthesize();模块的文件布局
模块可以放在单独的文件或目录中。三种组织方式:
方式一:内联模块
// src/lib.rs
mod network {
fn connect() {}
}方式二:同目录下的文件
// src/lib.rs
mod network; // 编译器查找 src/network.rs 或 src/network/mod.rs
// src/network.rs
pub fn connect() {}
pub fn disconnect() {}方式三:目录 + mod.rs(传统风格)
src/
├── lib.rs
└── network/
├── mod.rs # network 模块的内容
├── client.rs # network::client 子模块
└── server.rs # network::server 子模块方式四:目录 + 同名文件(新版风格)
src/
├── lib.rs
├── network.rs # network 模块的内容
└── network/
├── client.rs # network::client 子模块
└── server.rs # network::server 子模块方式四更现代——避免了许多 mod.rs 文件造成的混乱。两种风格功能完全等价,选择团队偏好的即可。
use:将名称引入作用域
use 关键字将路径中的项目带入当前作用域,简化访问:
// 不使用 use——完整路径
let mut v = std::collections::HashMap::new();
v.insert("key", 1);
// 使用 use——简化访问
use std::collections::HashMap;
let mut v = HashMap::new();
v.insert("key", 1);use 的多种用法:
// 导入多个项目
use std::collections::{HashMap, HashSet, BTreeMap};
// 导入所有公开项目(谨慎使用——命名空间污染)
use std::io::prelude::*;
// 别名——解决命名冲突
use std::io::Result as IoResult;
use std::fmt::Result as FmtResult;
// 导入模块本身(不是模块里的内容)
use std::io; // 之后可以用 io::stdin(), io::stdout()
// self 导入自身——用在 {} 批量导入中
use std::io::{self, Read, Write}; // self = std::io路径:绝对 vs 相对
Rust 的路径可以绝对或相对:
// 绝对路径(推荐)
use crate::network::client::connect;
use crate::network::server::listen;
// 使用 self(当前模块)
use self::network::client::connect;
// 使用 super(父模块)
use super::utils::format_address;
// 外部 crate
use serde::{Serialize, Deserialize};路径关键字:
| 关键字 | 含义 |
|---|---|
crate | 当前 crate 的根 |
self | 当前模块 |
super | 父模块 |
:: 开头 | 外部 crate 或标准库 |
推荐默认使用 crate:: 开头的绝对路径——它们的含义不随文件移动而改变。
模块的独立作用域
每个模块从一个干净的作用域开始——它看不到外部作用域中的名称,除非显式导入:
// 外层作用域
use std::collections::HashMap;
mod my_module {
// 这里看不到 HashMap——需要重新 use
use std::collections::HashMap;
pub fn create_map() -> HashMap<String, i32> {
HashMap::new()
}
}这个设计确保了模块的自包含性——读取一个 mod 时,不需要翻看其定义位置的「环境」。
pub use:重新导出
pub use 使一个项目在模块中可用,同时从模块重新导出:
mod plant {
pub mod roots {
pub fn absorb() {}
}
pub mod leaves {
pub fn photosynthesize() {}
}
// 重新导出:外部可以直接用 plant::absorb() 和 plant::photosynthesize()
pub use roots::absorb;
pub use leaves::photosynthesize;
}
// 调用者无需知道内部模块结构
plant::absorb();
plant::photosynthesize();这是 Rust 标准库 prelude 的实现机制——std::prelude::v1 使用大量 pub use 把常用项目提升到顶层。
公开结构体的字段
即使结构体是 pub 的,它的每个字段默认都是私有的:
pub struct Fern {
pub roots: RootSet, // 公开——外部可以直接访问
pub stems: StemSet, // 公开
leaves: LeafSet, // 私有——只有 Fern 所在模块可以访问
health: f64, // 私有
}这种设计消除了 Java/C++ 中 getter/setter 的 boilerplate——只需将允许外部访问的字段标为 pub。对于需要不变量保护的字段,保持私有并提供方法。
将程序转为库
把一个单文件程序变成可复用的库只需三步:
- 将
src/main.rs重命名为src/lib.rs - 将公共 API 的函数和类型标为
pub - 在
Cargo.toml中保留[package]元数据(Cargo 自动检测lib.rs)
// src/lib.rs
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();
}
}如果要保留可执行文件,同时提供库,将 main 放在 src/main.rs 或 src/bin/ 下:
// src/bin/fern_sim.rs
use fern_sim::{Fern, run_simulation};
fn main() {
let mut fern = Fern {
size: 1.0,
growth_rate: 0.001,
};
run_simulation(&mut fern, 365);
println!("Final size: {}", fern.size);
}测试与文档
单元测试
在模块内嵌测试,用 #[cfg(test)] 条件编译:
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
#[should_panic(expected = "assertion failed")]
fn test_panic() {
assert!(false);
}
#[test]
fn test_with_result() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err("math is broken".to_string())
}
}
}cargo test 并行运行测试。cargo test test_add 按名称筛选。
集成测试
放在 tests/ 目录下的 .rs 文件被作为独立的 crate 编译——它们只能访问库 crate 的公共 API:
// tests/integration_test.rs
use my_lib::add;
#[test]
fn test_add_integration() {
assert_eq!(add(2, 3), 5);
}文档注释
/// 注释生成文档,支持 Markdown:
/// 计算两个数的最大公约数。
///
/// # 示例
///
/// ```
/// let result = my_math::gcd(48, 18);
/// assert_eq!(result, 6);
/// ```
///
/// # Panics
///
/// 不会 panic。
pub fn gcd(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}代码块会自动作为文档测试运行——cargo test 会验证它们。
//! 注释用于模块/crate 级别的文档:
//! # My Math Library
//!
//! 提供基本的数学运算。
//!
//! ## 模块
//!
//! - `gcd` — 最大公约数
//! - `fib` — 斐波那契数列用 cargo doc --open 生成并打开 HTML 文档。
依赖管理
指定依赖
[dependencies]
# 从 crates.io
serde = "1.0"
# 从 Git 仓库
image = { git = "https://github.com/example/image.git", rev = "abc123" }
# 从本地路径
my_local_lib = { path = "../my_local_lib" }
# 带 feature
serde = { version = "1.0", features = ["derive"] }
# 可选依赖(条件编译)
gzip = { version = "0.6", optional = true }语义化版本
Cargo 遵循语义化版本(SemVer):
| 版本变化 | 兼容性 |
|---|---|
| 主版本(1.x → 2.0) | 不兼容的 API 变化 |
| 次版本(1.0 → 1.1) | 向后兼容的新功能 |
| 补丁版本(1.0.0 → 1.0.1) | 向后兼容的 Bug 修复 |
特殊规则:
0.0.x— 与其他所有版本不兼容0.x.y— 仅在 0.x 系列内兼容
Cargo.lock
Cargo.lock 记录每个依赖的精确版本,保证可重现构建。对二进制项目应该提交到版本控制;对库项目可以不提交(由下游用户生成)。
cargo update 用兼容的最新版本更新 Cargo.lock。
工作空间(Workspace)
多个相关的 crate 可以组织为工作空间,共享 target/ 目录和 Cargo.lock:
# 根 Cargo.toml
[workspace]
members = [
"fern_sim", # 主库
"fern_img", # 图像处理工具
"fern_video", # 视频工具
]工作空间级别的命令:
cargo build --workspace # 构建所有成员
cargo test --workspace # 测试所有成员
cargo doc --workspace # 为所有成员生成文档所有成员共享同一个 target/ 目录——节省磁盘空间,加速增量编译。
发布到 crates.io
将 crate 发布到 Rust 的公共注册表:
- 在 crates.io 注册账号
- 用
cargo login配置 API 令牌 - 确保
Cargo.toml包含所有必需的元数据(license、description、repository等) cargo publish
发布前用 cargo package 验证 crate 能否正确打包:
cargo package --no-verify # 创建 .crate 文件用于检查注意:发布后不能删除——只能 yank(标记为不可用于新项目,但已有依赖仍然有效):
cargo yank --vers 0.1.0
cargo yank --vers 0.1.0 --undo # 撤销 yank属性
属性是给编译器的指令,语法为 #[attr] 或 #:
// 条件编译
#[cfg(target_os = "linux")]
fn platform_specific() { /* Linux 版本 */ }
#[cfg(target_os = "windows")]
fn platform_specific() { /* Windows 版本 */ }
// 抑制警告
#[allow(dead_code)]
fn unused_but_kept() {}
// 内联提示
#[inline]
fn hot_function() {}
// 必须内联
#[inline(always)]
fn critical_path() {}
// 禁止内联
#[inline(never)]
fn cold_path() {}
// Crate 级别的属性
#![allow(non_camel_case_types)] // 整个 crate 允许非驼峰类型名常用的条件编译选项:
#[cfg(...)] 条件 | 含义 |
|---|---|
target_os = "..." | 目标操作系统 |
target_arch = "..." | 目标架构 |
feature = "..." | 启用的 feature |
debug_assertions | debug 模式 |
test | 测试模式 |
unix / windows | 平台族 |
项目组织最佳实践
一个中大型 Rust 项目的推荐布局:
my_project/
├── Cargo.toml # 工作空间根配置
├── Cargo.lock
├── src/
│ ├── lib.rs # 库根:pub mod + pub use 暴露 API
│ ├── main.rs # 二进制入口
│ ├── error.rs # 错误类型定义
│ ├── config.rs # 配置解析
│ ├── models/ # 数据模型
│ │ ├── mod.rs
│ │ ├── user.rs
│ │ └── product.rs
│ ├── services/ # 业务逻辑
│ │ ├── mod.rs
│ │ ├── auth.rs
│ │ └── payment.rs
│ └── utils/ # 工具函数
│ └── mod.rs
├── tests/ # 集成测试
│ ├── api_test.rs
│ └── model_test.rs
├── examples/ # 示例程序
│ └── simple.rs
└── benches/ # 性能基准测试
└── benchmark.rs核心原则:
lib.rs声明所有模块,用pub use控制公开 API。- 每个
.rs文件对应一个模块(mod x;自动查找x.rs)。 - 按功能而非类型分模块(
models/user.rs而非structs.rs)。 - 集成测试放
tests/(只能测公开 API),单元测试放源码中(#[cfg(test)])。 Cargo.lock对二进制项目提交,对库项目可以考虑不提交。
小结
- Crate 是编译和分发的基本单元:二进制 crate(
main.rs)产生可执行文件,库 crate(lib.rs)产生.rlib。Cargo 管理依赖解析和构建。 - 模块提供命名空间:
mod定义层次结构,默认私有。可见性修饰符pub、pub(crate)、pub(super)精确控制访问。 use将名称引入作用域:crate::绝对路径推荐使用;super::访问父模块;{}批量导入;as别名。- 模块文件布局灵活:内联、单独文件、目录 +
mod.rs、目录 + 同名文件四种方式。 pub use重新导出:隐藏内部模块结构,提供扁平化的公共 API。- 测试与文档内建:
#[test]单元测试、tests/集成测试、///和//!文档注释自动生成 HTML 文档并可测试代码块。 Cargo.toml管理依赖:crates.io、Git、本地路径。语义化版本 +Cargo.lock保证可重现构建。工作空间让多个 crate 共享编译缓存。