3. 模块化与代码组织

当 Rust 项目从单个 main.rs 增长到数千行代码时,把所有逻辑堆在一个文件里会迅速失控:编译错误定位困难、函数命名冲突、团队协作互相踩踏、单元测试难以隔离。Rust 提供了一整套模块化机制来解决这个问题——模块(mod)负责划分命名空间,可见性(pub)控制边界,路径(crate::、super::、self::)负责引用,而 Cargo 工作空间(workspace)则把多个 crate 组织成一个可统一构建的整体。本节围绕这四层机制,讲清楚它们各自解决什么问题、如何配合使用。

模块:命名空间与文件布局

模块的本质是给代码建立一棵树。根节点是 crate 本身,每个 mod 声明都是树上的一个分支。Rust 2018 之后,模块与文件系统的映射规则非常直接:mod foo; 会去寻找同目录下的 foo.rs 或 foo/mod.rs;如果 foo 还有子模块,则放在 foo/ 目录下。推荐使用 foo.rs + foo/bar.rs 的形式,避免 mod.rs 在编辑器标签页里难以区分。

一个典型的分层结构如下:

src/
├── main.rs          // crate 根,声明 mod
├── config.rs        // 配置加载
├── parser/
│   ├── mod.rs       // 或 parser.rs
│   ├── lexer.rs
│   └── ast.rs
└── storage/
    ├── mod.rs
    └── file.rs

在 main.rs 中写 mod config; mod parser; mod storage;,就完成了模块树的挂载。注意:模块声明只是“登记”,并不自动把子模块暴露给外部;子模块还需要在父模块中显式声明。

可见性:pub 的边界控制

Rust 默认所有项都是私有的,只有显式标注 pub 才能被外部访问。可见性有四个层级:pub(完全公开)、pub(crate)(仅当前 crate 可见)、pub(super)(仅父模块可见)、pub(in path)(指定路径内可见)。实际工程中最常用的是 pub(crate):它让内部模块之间可以互相调用,但不会把实现细节泄漏给下游依赖者。

一个常见的错误是给所有东西都加 pub,导致 API 表面无限膨胀。更好的做法是:结构体字段默认私有,只暴露必要的 getter;内部辅助函数用 pub(crate);只有真正需要被外部 crate 使用的类型和函数才用 pub。这样编译器会帮你守住封装边界。

路径:crate、super、self 的引用规则

路径是模块树上的“地址”。Rust 提供三种前缀:crate:: 表示从当前 crate 根开始,super:: 表示父模块,self:: 表示当前模块。绝对路径用 crate:: 最清晰,相对路径用 super:: 适合在子模块中引用兄弟模块。此外,use 语句可以把长路径引入作用域,减少重复书写。

需要特别注意 use 的可见性:use 默认是私有的,如果希望外部也能通过 crate::foo::Bar 访问,需要写 pub use。这正是“重导出”(re-export)的机制——它允许你把内部模块的公共 API 聚合到一个更稳定的路径下,从而在不破坏调用方的前提下重构内部结构。

模块树、可见性与路径引用关系 crate 根 (main.rs) mod config; mod parser; mod storage; config pub struct AppConfig pub fn load() parser pub mod lexer; pub mod ast; storage pub(crate) fn save() pub fn open() lexer pub fn tokenize() ast pub enum Node 路径引用示例 crate::config::AppConfig::load() super::lexer::tokenize() // 在 ast 模块中引用兄弟模块 pub use crate::parser::ast::Node; // 重导出,外部可用 crate::Node pub:完全公开 pub(crate):仅本 crate 可见

工作空间:多 crate 拆分与复用

当项目包含可执行程序、库、以及多个共享组件时,单个 crate 就不够用了。Cargo 工作空间允许在一个根目录下管理多个 crate,共享同一个 Cargo.lock 和 target/ 目录,从而避免重复编译依赖。根目录的 Cargo.toml 只需声明成员:

[workspace]
members = [
    "app",
    "core",
    "utils",
]
resolver = "2"

其中 core 和 utils 是库 crate,app 是二进制 crate。在 app/Cargo.toml 中通过路径依赖引用它们:

[dependencies]
core = { path = "../core" }
utils = { path = "../utils" }

这样 app 就可以使用 core:: 和 utils:: 的公共 API。工作空间的核心价值在于:统一版本、统一构建、统一测试。运行 cargo build 会编译所有成员,cargo test --workspace 会跑全部测试。对于团队协作,还可以在根 Cargo.toml 中用 [workspace.dependencies] 集中声明依赖版本,各成员用 workspace = true 继承,避免版本漂移。

复用策略:从重导出到 feature 开关

模块化最终服务于复用。除了 pub use 重导出,Rust 还提供 feature 开关来控制可选功能。例如在 core/Cargo.toml 中定义:

[features]
default = ["json"]
json = ["dep:serde_json"]
yaml = ["dep:serde_yaml"]

调用方按需启用:core = { path = "../core", features = ["yaml"] }。这样既保持了 API 的稳定性,又避免了把不需要的依赖强加给下游。结合 pub(crate) 的边界控制,一个健康的 Rust 工程应该做到:内部实现自由重构,对外接口稳定收敛,构建产物按需裁剪。

模块化不是一次性设计,而是随项目演进的持续过程。建议每新增一个功能域就新建一个模块,每发现两个模块互相引用超过三次就考虑提取公共 crate。保持模块树浅而清晰,比追求“完美分层”更重要。