Skip to content

Rust 模块系统

基于树状层级的作用域控制

Rust 的模块系统与 TypeScript 有根本区别:

  • TS: 文件系统驱动,一个文件即一个模块
  • Rust: 作用域驱动,通过 mod 关键字显式声明模块树,从根(lib.rsmain.rs)开始类似 DNS 查询式的寻址

核心特点:

  • 模块是逻辑概念,不仅仅是物理文件
  • 必须在模块树中显式声明才能被访问(默认私有)
  • 权限控制在模块声明层面(pub mod vs mod)
  • 同一文件可包含多个模块

定义

  • workspace: 多个 package 的集合(多个 Cargo.toml),可以包含多个 library crate 和多个 binary crate
  • package: 由一个 Cargo.toml 定义,只能有一个 library crate,但可以有多个 binary crate
  • crate: 最小编译单元(main.rslib.rs)
    • Library crate: 生成库文件(.rlib.so 等),默认入口为 lib.rs
    • Binary crate: 生成可执行文件,入口为 main.rs
  • module: crate 内的代码组织单位,通过 mod 关键字定义,构成模块树

关键区别:

  • Workspace 是项目级概念,用于多 package 协作(如单体仓库)
  • Package 是构建级概念,由 Cargo.toml 管理依赖和配置
  • Crate 是编译级概念,编译器的处理对象
  • Module 是代码级概念,逻辑上的作用域划分

层次结构

bash
workspace
> package
> crate
> module

声明模块

  1. 使用 mod 关键字声明模块
  2. 使用 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 模块内容的逻辑如下:

  1. 第一步: 查找同级的 a.rs.
  2. 第二步: 如果没找到 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.rssrc/storage.rs 寻找模块定义.

2. 在 src/network.rs:

rust
pub mod client;
pub mod server;

编译器会去 src/network/ 目录下找 client.rsserver.rs.

为什么这样优于 TypeScript?

在 TS 中,文件夹只是物理容器.在 Rust 中,这种结构提供了两个关键优势:

  • 权限收口: network/ 目录下的所有细节必须通过 network.rspub 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

基于 MIT 协议发布