Rust 核心概念(八):模块系统
模块系统不是语法细节,它决定了项目结构能不能长期维护。
package、crate、module
先区分三个词。
package 是 Cargo 管理、构建和发布的项目单元。
一个 package 里有 Cargo.toml。
crate 是 Rust 编译单元,也是 Rust 做名称解析、可见性检查、依赖连接和最终产物生成的基本边界。
crate 分两类:
- binary crate:编译成可执行程序。
- library crate:编译成库。
一个 package 可以同时包含一个 library crate 和多个 binary crate。
例如:
1 | |
这里通常会产生这些 crate:
src/lib.rs:library crate。src/main.rs:默认 binary crate。src/bin/worker.rs:名为worker的 binary crate。src/bin/admin.rs:名为admin的 binary crate。
这些 crate 属于同一个 package,但它们是不同的编译入口。
module 是 crate 内部组织代码的方式。
简单理解:
- package:项目。
- crate:编译单元和 API 边界。
- module:代码命名空间。
这三个层次不要混在一起:
- Cargo 先根据
Cargo.toml找到 package 和目标 crate。 - 编译器从每个 crate root 开始构建模块树。
- 模块树里的 item 再通过
pub、use和路径规则组织起来。
crate root
crate root 是编译器开始构建模块树的入口。
常见入口是:
src/main.rs:binary crate 的 crate root。src/lib.rs:library crate 的 crate root。
main.rs 就是这个 binary crate 的根,lib.rs 就是这个 library crate 的根。
crate root 不是“项目根目录”,而是某个 crate 的模块树根节点。
例如:
1 | |
如果 lib.rs 中写:
1 | |
那么 user 是 library crate 的子模块。
如果 main.rs 中也写:
1 | |
那么这个 user 是 binary crate 的子模块。
它们读取的文件路径可能相同,但处在不同 crate 的模块树里。真实项目通常不要在 main.rs 和 lib.rs 里重复声明同一套模块,而是让 binary crate 通过 package 的 library crate 使用公共逻辑:
1 | |
mod
mod 用来声明模块。
1 | |
这里 user 是一个模块。
模块形成一棵树。
文件模块
真实项目不会把所有模块都写在一个文件里。
如果在 src/main.rs 里写:
1 | |
Rust 会去找:
1 | |
然后在 src/user.rs 中写:
1 | |
这就是最常见的拆文件方式。
子模块目录
如果模块继续变大,可以放到目录里。
1 | |
在 main.rs 中:
1 | |
在 user/mod.rs 中:
1 | |
在 user/service.rs 中:
1 | |
也可以使用较新的文件布局:
1 | |
在 user.rs 中声明:
1 | |
两种方式都能用,项目里保持一致更重要。
pub
Rust 默认私有。模块里的 item 如果不加 pub,外部模块不能访问。
1 | |
受限可见性
pub 表示对外公开,但真实项目里经常需要更细的范围。
常见写法:
1 | |
含义是:
pub(crate):当前 crate 内可见。pub(super):父模块可见。
pub struct 的字段
结构体本身 pub,不代表字段也是 pub。
1 | |
外部可以构造或访问 name,但不能访问 age。
use
use 用来把路径引入当前作用域。
1 | |
没有 use 时也能写完整路径:
1 | |
use 只是让代码更短,不改变可见性。
use 的作用域也是词法作用域。在哪个模块里写 use,名字就主要在那个模块里可用:
1 | |
外层模块不会因为子模块写了 use HashMap 就也能直接写 HashMap。
在函数内部写 use 也可以:
1 | |
这种写法适合只在局部使用的名字。
prelude
有些类型和 trait 平时没写 use 也能直接用,例如:
1 | |
这里的 Vec、String、Result、Option、Some、None、Ok、Err 以及很多常用 trait,并不是语言关键字,而是标准库预导入的一部分。
Rust 会给每个 crate 自动引入一组常用名字,这组名字叫 prelude。
可以粗略理解为编译器在每个模块前面帮你放了一些常用导入:
1 | |
这只是理解模型,不是要求你真的手写这一行。
prelude 解决的是常用 API 的噪音问题。如果没有 prelude,很多基础代码会变成:
1 | |
prelude 里还包含一些 trait。trait 被导入后,它的方法才能参与方法调用解析。
例如 parse 依赖 FromStr:
1 | |
你通常不用写:
1 | |
因为相关能力已经通过 prelude 进入作用域。
但 prelude 不是“自动导入整个标准库”。例如 HashMap 不在标准 prelude 中,所以仍然要写:
1 | |
外部 crate 也不会自动进入作用域。依赖写进 Cargo.toml 只是告诉 Cargo 下载和连接它,代码里仍然要通过路径或 use 使用:
1 | |
1 | |
一些框架会提供自己的 prelude,例如:
1 | |
或者:
1 | |
这种 prelude 不是编译器自动导入的,而是库作者为了集中暴露常用 trait、类型和扩展方法提供的普通模块。它的本质仍然是 use。
- 标准库 prelude:编译器按 edition 自动导入。
- 第三方库 prelude:你显式
use xxx::prelude::*。 - 普通
use:按需引入具体路径。
as
如果名字冲突,可以使用 as 重命名。
1 | |
这样可以同时使用两个同名类型。
re-export
模块可以重新导出内部 item。
1 | |
pub use 常用于整理对外 API。
内部可以有复杂目录结构,对外暴露一个更简单的入口。
super、self、crate
路径里常见三个关键字:
crate:当前 crate 根。self:当前模块。super:父模块。
例如:
1 | |