Skip to content
Published at:

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 的元数据和依赖:

toml
[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年份关键变化
20152015初始版本
20182018async/await 关键字,模块路径改革,dyn Trait
20212021闭包捕获优化,IntoIterator for arrays

不同 Edition 的 crate 可以无缝互操作——Edition 是每个 crate 的独立选择。用 cargo fix 可以自动迁移代码到新 Edition。

Build Profile

Cargo 提供标准的构建配置:

toml
[profile.dev]       # cargo build(默认)
opt-level = 0       # 不优化,快速编译
debug = true        # 包含调试信息

[profile.release]   # cargo build --release
opt-level = 3       # 激进优化
debug = false       # 去掉调试信息
lto = true          # 链接时优化

可以在 Cargo.toml 中覆盖这些设置:

toml
[profile.release]
debug = true  # release build 也包含调试信息

模块

模块是 Rust 的命名空间机制——控制项目(函数、类型、常量等)的组织和可见性。

mod 定义模块

rust
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) 用于非常精确的控制:

rust
mod plant_structures {
    pub mod roots {
        pub(in crate::plant_structures) struct Cytokinin {
            // 仅 plant_structures 模块内部可访问
        }
    }
}

模块可以嵌套:

rust
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();

模块的文件布局

模块可以放在单独的文件或目录中。三种组织方式:

方式一:内联模块

rust
// src/lib.rs
mod network {
    fn connect() {}
}

方式二:同目录下的文件

rust
// 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 关键字将路径中的项目带入当前作用域,简化访问:

rust
// 不使用 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 的多种用法:

rust
// 导入多个项目
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 的路径可以绝对或相对:

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:: 开头的绝对路径——它们的含义不随文件移动而改变。

模块的独立作用域

每个模块从一个干净的作用域开始——它看不到外部作用域中的名称,除非显式导入:

rust
// 外层作用域
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 使一个项目在模块中可用,同时从模块重新导出:

rust
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 的,它的每个字段默认都是私有的:

rust
pub struct Fern {
    pub roots: RootSet,    // 公开——外部可以直接访问
    pub stems: StemSet,    // 公开
    leaves: LeafSet,       // 私有——只有 Fern 所在模块可以访问
    health: f64,           // 私有
}

这种设计消除了 Java/C++ 中 getter/setter 的 boilerplate——只需将允许外部访问的字段标为 pub。对于需要不变量保护的字段,保持私有并提供方法。

将程序转为库

把一个单文件程序变成可复用的库只需三步:

  1. src/main.rs 重命名为 src/lib.rs
  2. 将公共 API 的函数和类型标为 pub
  3. Cargo.toml 中保留 [package] 元数据(Cargo 自动检测 lib.rs
rust
// 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.rssrc/bin/ 下:

rust
// 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)] 条件编译:

rust
// 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:

rust
// tests/integration_test.rs
use my_lib::add;

#[test]
fn test_add_integration() {
    assert_eq!(add(2, 3), 5);
}

文档注释

/// 注释生成文档,支持 Markdown:

rust
/// 计算两个数的最大公约数。
///
/// # 示例
///
/// ```
/// 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 级别的文档:

rust
//! # My Math Library
//!
//! 提供基本的数学运算。
//!
//! ## 模块
//!
//! - `gcd` — 最大公约数
//! - `fib` — 斐波那契数列

cargo doc --open 生成并打开 HTML 文档。

依赖管理

指定依赖

toml
[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

toml
# 根 Cargo.toml
[workspace]
members = [
    "fern_sim",    # 主库
    "fern_img",    # 图像处理工具
    "fern_video",  # 视频工具
]

工作空间级别的命令:

bash
cargo build --workspace   # 构建所有成员
cargo test --workspace    # 测试所有成员
cargo doc --workspace     # 为所有成员生成文档

所有成员共享同一个 target/ 目录——节省磁盘空间,加速增量编译。

发布到 crates.io

将 crate 发布到 Rust 的公共注册表:

  1. crates.io 注册账号
  2. cargo login 配置 API 令牌
  3. 确保 Cargo.toml 包含所有必需的元数据(licensedescriptionrepository 等)
  4. cargo publish

发布前用 cargo package 验证 crate 能否正确打包:

bash
cargo package --no-verify  # 创建 .crate 文件用于检查

注意:发布后不能删除——只能 yank(标记为不可用于新项目,但已有依赖仍然有效):

bash
cargo yank --vers 0.1.0
cargo yank --vers 0.1.0 --undo  # 撤销 yank

属性

属性是给编译器的指令,语法为 #[attr]#![attr](crate 级别):

rust
// 条件编译
#[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_assertionsdebug 模式
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 定义层次结构,默认私有。可见性修饰符 pubpub(crate)pub(super) 精确控制访问。
  • use 将名称引入作用域crate:: 绝对路径推荐使用;super:: 访问父模块;{} 批量导入;as 别名。
  • 模块文件布局灵活:内联、单独文件、目录 + mod.rs、目录 + 同名文件四种方式。
  • pub use 重新导出:隐藏内部模块结构,提供扁平化的公共 API。
  • 测试与文档内建#[test] 单元测试、tests/ 集成测试、/////! 文档注释自动生成 HTML 文档并可测试代码块。
  • Cargo.toml 管理依赖:crates.io、Git、本地路径。语义化版本 + Cargo.lock 保证可重现构建。工作空间让多个 crate 共享编译缓存。