Rust 模块系统
基于树状层级的作用域控制
Rust 的模块系统与 TypeScript 有根本区别:
- TS: 文件系统驱动,一个文件即一个模块
- Rust: 作用域驱动,通过
mod关键字显式声明模块树,从根(lib.rs或main.rs)开始类似 DNS 查询式的寻址
核心特点:
- 模块是逻辑概念,不仅仅是物理文件
- 必须在模块树中显式声明才能被访问(默认私有)
- 权限控制在模块声明层面(
pub modvsmod) - 同一文件可包含多个模块
定义
workspace: 多个 package 的集合(多个Cargo.toml),可以包含多个 library crate 和多个 binary cratepackage: 由一个Cargo.toml定义,只能有一个 library crate,但可以有多个 binary cratecrate: 最小编译单元(main.rs或lib.rs)Library crate: 生成库文件(.rlib、.so等),默认入口为lib.rsBinary crate: 生成可执行文件,入口为main.rs
module: crate 内的代码组织单位,通过mod关键字定义,构成模块树
关键区别:
Workspace是项目级概念,用于多package协作(如单体仓库)Package是构建级概念,由Cargo.toml管理依赖和配置Crate是编译级概念,编译器的处理对象Module是代码级概念,逻辑上的作用域划分
层次结构
bash
workspace
⨽> package
⨽> crate
⨽> module声明模块
- 使用
mod关键字声明模块 - 使用
pub mod将模块公有化
注意事项:
- 一个文件可以包含多个
mod声明 - 文件头部的
mod声明本质是将指定位置的代码"贴入"当前作用域 - 模块默认私有,必须显式使用
pub mod才能被外部访问
导入导出核心机制
lib.rs: 统一声明和导出所有模块(crate 的根)pub: 导出项(函数、结构体等)为公有pub mod: 导出子模块为公有pub use: 重导出(创建别名或隐藏内部结构)use: 导入,创建路径的快捷方式(非移动所有权)
核心理解:
Rust 模块系统与 TypeScript 的根本区别在于:
- TS: 基于文件系统,文件即模块
- Rust: 基于作用域,通过显式声明从根开始构建模块树
这种设计让 Rust 在大型项目中能更好地控制可见性和依赖关系.
导入导出最佳实践
1. 结构体 (Struct) 和 枚举 (Enum)
- 惯例:直接导入到当前作用域
- 因为结构体和枚举通常代表一个"名词",名字本身就很有辨识度
- 做法:
use std::collections::HashMap; - 使用:
let map = HashMap::new();
2. 函数 (Function)
- 惯例:保留一层父级模块名
- 直接导入函数名容易混淆是本地函数还是外部库函数
- 做法:
use std::fs;(而不是use std::fs::read;) - 使用:
fs::read("file.txt")?;(一眼看出是文件系统的 read,而不是自定义的函数)
3. Trait (接口)
- 惯例:直接导入或导入整个 Prelude
- Trait 方法必须 use 才能调用,一般直接导入需要的 Trait,或者使用库提供的预导模块(Prelude)一次性导入常用 Trait
- 做法:
use std::io::Read; - 使用:
file.read_to_string(&mut s)?;(导入后,方法就"挂载"上去了)
路径规则
crate::: 从当前 crate 根开始(类似项目根目录)self::: 当前模块(类似./)super::: 父模块(类似../)
可见性规则
- 子模块可访问父模块的所有项,但父模块不可访问子模块的项(除非明确
pub) Struct: 字段需要pub导出才能外部访问Enum: 变体无需pub(枚举的所有变体对外部一视同仁)lib.rs推荐从crate::路径引入Binary crate推荐使用项目包名引入
模块文件的查找规则
当你写下 mod a; 时,编译器查找 a 模块内容的逻辑如下:
- 第一步: 查找同级的
a.rs. - 第二步: 如果没找到
a.rs,查找同级a/mod.rs(旧版习惯,现已不推荐).
现代风格 vs 旧版风格
现代风格(推荐):
bash
src/
├── main.rs <-- 声明 mod a;
├── a.rs <-- 声明 pub mod b;
└── a/
└── b.rs <-- 存放常量的地方 或者 模块旧版风格(不推荐):
bash
src/
├── main.rs <-- 声明 mod a;
└── a/
├── mod.rs/a.rs <-- 声明 pub mod b;
└── b.rs <-- 存放常量的地方 或者 模块推荐的项目结构
bash
src/
├── main.rs # 1. 根模块: 只注册顶层业务 (mod network, mod storage)
├── lib.rs # 可选: 如果同时提供库和二进制
├── network.rs # 2. network 模块的入口文件
├── network/ # 3. network 的具体子模块全放这里
│ ├── client.rs
│ └── server.rs
├── storage.rs # 4. storage 模块的入口文件
└── storage/
├── mysql.rs
└── redis.rs模块注册流程
1. 在 src/main.rs 中:
rust
mod network;
mod storage;编译器会去 src/network.rs 和 src/storage.rs 寻找模块定义.
2. 在 src/network.rs 中:
rust
pub mod client;
pub mod server;编译器会去 src/network/ 目录下找 client.rs 和 server.rs.
为什么这样优于 TypeScript?
在 TS 中,文件夹只是物理容器.在 Rust 中,这种结构提供了两个关键优势:
权限收口:
network/目录下的所有细节必须通过network.rs的pub mod声明才能外部访问.network.rs充当了模块的 API 网关.重导出 (Re-export): 可以在
network.rs中编写pub use self::client::Client;.调用者只需use crate::network::Client;,隐藏了内部实现细节.
最佳实践总结
- 用一个
a.rs文件管理一个a/文件夹 a.rs负责声明和控制子模块的可见性a/文件夹存放具体的代码实现- 优先使用
pub use为外部提供稳定的 API 入口
关于 struct 和 trait
- 要使用
struct实现的trait方法,需要导入struct的同时也导入trait - 即使外部库已经为它的对象写好了
impl Trait for Struct,你在使用时也必须同时引入这两者 - Prelude 模式: 大多数成熟的 Rust 库(如 tokio、itertools、diesel)都会提供一个预导模块 (Prelude)rust
// 一次性把这个库最常用的所有 Trait 都引入进来 use some_library::prelude::*;
原因:
1. 避免方法名冲突(命名空间管理)
- 如果同时使用两个库,它们都给
String增加了parse()方法 - 如果 Rust 默认自动加载所有 Trait 方法,编译器就不知道你调用的是哪个
- 通过强制
use,你明确了想使用哪个库的功能
2. 编译性能优化
- 如果编译器每次处理文件都要扫描项目中成百上千个 Trait,寻找匹配的方法,编译速度会无法接受
- 显式导入只加载必要的 Trait,减少编译时间和内存占用
3. 鸭子类型的权衡
- Rust 采用显式导入而非隐式加载,确保代码的确定性和可读性
- 看到
use std::io::Read;就知道后续调用的read()方法来自该 Trait