Skip to content

Rust 模块系统 ​

概要 ​

Rust 模块系统 主要解决三个问题:代码组织、封装边界和命名空间。它用于把相关定义归到一组、控制内容的对外可见性,并避免名称冲突。

层级关系如下:

text
Package(Cargo.toml 管理的项目)
└── Crate(编译单元,入口如 lib.rs、main.rs、src/bin/*.rs)
    └── Module(由 mod 声明,可嵌套)
        ├── 子模块
        ├── 路径:crate::、self::、super::
        └── 可见性
            ├── 默认私有:仅定义它的模块及其子模块可访问
            ├── pub:完全公开
            ├── pub(crate):当前 crate 可见
            ├── pub(super):父模块可见
            └── pub(in path):指定祖先模块可见

一、Package 与 Crate ​

模块系统的顶层概念:Crate 是编译单元,Package 是 Cargo 管理的项目包。

  • Package(包):由 Cargo.toml 定义,cargo new 创建,一个 Package 可以包含多个 crate:
    • 最多 1 个 library crate(根文件 src/lib.rs)
    • 0 个或多个 binary crate(根文件 src/main.rs 或 src/bin/*.rs)
  • Crate(编译单元):Rust 编译器的编译单元,编译产出库或者可执行程序。
    • Library crate (库):不自带 main,供其他代码引用,入口 src/lib.rs
    • Binary crate (可执行程序):需要 main 函数,入口 src/main.rs / src/bin/*.rs
  • Crate root:编译器开始解析代码的入口文件,模块查找从 crate root 开始。

cargo new my-app 默认创建的是 binary crate 的 Package。若要创建供他人使用的库,用 cargo new my-lib --lib,这会生成 src/lib.rs 而不是 src/main.rs。

  • Module(模块 mod)

把一堆相关代码打包分组,解决名字冲突、控制能不能被外面调用。用 mod 声明子模块时,编译器会从 crate root 开始搜索文件。

类比:Package = 小区;Crate = 一栋楼;Module = 楼里面不同房间。房间用来把东西分类存放,还可以锁门(私有)不让外人进来。

text
my-app/
├── Cargo.toml          # 定义 Package
└── src/
    ├── main.rs          # binary crate root:程序入口,负责启动
    └── lib.rs           # library crate root:核心逻辑、对外API

一个 Package 同时包含 main.rs 和 lib.rs 是很常见的架构:lib.rs 承载核心逻辑和对外 API,main.rs 只负责依赖注入和启动。这样,代码既能作为二进制运行,也能被其他项目作为库引用。

二、定义模块: mod ​

mod 关键字用来声明一个模块。Rust 支持三种写法:

1. 内联模块 ​

所有代码挤在同一个文件,模块体直接写在大括号内,适合嵌套定义小模块或示例代码:

rust
mod net {
    pub fn connect(){} // pub=对外开放
    fn inner(){} // 默认锁门私有,只能net内部用
}
fn main(){
    net::connect();
}

2. 文件模块 ​

比如写 mod garden;,编译器就会去找同目录的garden.rs,把这个文件当作 garden 模块。

rust
mod garden; // 编译器会查找 src/garden.rs
fn main(){
    garden::water();
}
rust
// src/garden.rs(与 main.rs 同目录)
pub fn water(){ println!("浇水") }

3. 目录模块 ​

当模块太大需要进一步拆分时,用目录 + mod.rs 的形式:

text
src/
├── main.rs
└── garden/
    ├── mod.rs        # garden 模块的入口,声明子模块
    ├── vegetables.rs # garden 的子模块
    └── flowers.rs    # garden 的子模块
rust
pub mod vegetables;  // 声明子模块 → garden/vegetables.rs
pub mod flowers;      // 声明子模块 → garden/flowers.rs

pub fn water() { }
rust
mod garden;  // 编译器查找 src/garden/mod.rs

mod.rs 是模块的入口文件,相当于 Node.js 的 index.js。子模块在 mod.rs 中用 pub mod xxx; 声明后才对外可见——即使文件存在于目录中,没有 mod 声明就不会被编译。

注意

mod 声明的是逻辑树而非文件树。只创建 src/garden/vegetables.rs 文件、不在 mod.rs 中写 pub mod vegetables;,这个文件不会被编译——这是新手最容易踩的坑。

三、模块树与路径 ​

理解路径导航是攻克模块系统的关键。Rust 的模块树不是由文件系统自动生成的,把所有mod声明串起来,就形成一颗模块树,根节点就是 crate(main.rs/lib.rs)。

前缀作用类比文件路径
crate::绝对路径,从项目根(crate root)出发/根目录
self::当前自己这个模块./当前文件夹
super::父模块,往上跳一层../上一级
无前缀(如 hosting::)从当前模块开始,同 self::相对路径
text
src/
├── main.rs           # crate root
├── garden/
│   └── mod.rs
│   └── vegetables.rs
└── house/
    ├── mod.rs
    └── kitchen.rs
text
crate(根)
├── garden              ← mod garden;
│   └── vegetables      ← pub mod vegetables;(在 garden/mod.rs 中声明)
└── house               ← mod house;
    └── kitchen         ← pub mod kitchen;(在 house/mod.rs 中声明)

关键规则:mod garden; 只在 crate root(main.rs)中出现一次。mod vegetables; 只出现在 garden/mod.rs 中。子模块的路径前缀是从 crate root 到自身的完整路径——kitchen 的全路径是 crate::house::kitchen。

✅推荐:跨文件调用优先用crate::绝对路径。

好处:以后你挪代码块,路径不会坏掉;相对路径 super/self,一旦代码挪位置,全部报错。

1. 绝对路径: crate:: ​

从 crate root 开始导航,类似文件系统中的 /:

rust
// src/main.rs
mod garden;  // crate root 声明 garden 模块

fn main() {
    // 绝对路径:从 crate root 出发
    crate::garden::vegetables::plant("番茄");
}
rust
// src/garden/mod.rs
pub mod vegetables;
rust
// src/garden/vegetables.rs
pub fn plant(name: &str) {
    println!("种植 {name}");
}

crate:: 前缀在整个 crate 内部都有效。无论你在多深的子模块中,crate::garden::vegetables::plant() 指向的都是同一个函数。

函数 plant 完整绝对路径:crate::garden::vegetable::plant()

2. 相对路径: self:: 与 super:: ​

在同一模块内可以直接访问兄弟项,也可以用 self:: 显式标注当前模块,用 super:: 访问父模块:

rust
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() { println!("加入等位"); }
    }

    pub mod serving {
        pub fn take_order() {
            // super:: 回到父模块 front_of_house
            super::hosting::add_to_waitlist();

            // self:: 显式标注当前模块(不写也行,这里仅作示意)
            self::serve_order();
        }

        fn serve_order() { println!("上菜"); }
    }
}

选择建议

调同一个模块内的项用相对路径(短);跨多层调用用 crate::(移动代码时路径不会断裂)。无前缀的相对路径在代码重构时有隐患——如果你把一个函数移到别的模块,所有相对路径都要改,而 crate:: 始终有效。

四、可见性控制: pub ​

Rust 的可见性规则是 默认私有,显式公开——这与所有权的思想一脉相承:不让你访问的东西,编译器在编译期就拦住了。

1. 默认私有 ​

模块内的所有项(函数、结构体、枚举、常量等)默认仅模块内部可见。即使子模块也看不到父模块的私有项——除非用 pub 或更细粒度的修饰符显式公开。

2. pub — 完全公开 ​

rust
mod house {
    pub fn open_door() { println!("门开了"); }     // 所有人都能调用
    fn private_secret() { println!("秘密"); }        // 仅 house 内部可调用
}

fn main() {
    house::open_door();         // ✅ 公开,可访问
    // house::private_secret(); // ❌ 私有,编译错误
}

3. 分级可见性 ​

pub 不是唯一选项——Rust 提供了三个层级的可见性修饰符,让你精确控制暴露范围:

修饰符可见范围典型场景
pub所有人对外 API
pub(crate)当前 crate 内内部工具函数,不暴露给外部使用者
pub(super)父模块子模块向上级暴露,同级模块不可见
pub(in path)指定路径内精确控制(极少用)
rust
// src/lib.rs —— 一个 library crate
pub mod db {
    // 对外暴露的公共 API
    pub fn connect(url: &str) -> Connection { /* ... */ }

    // crate 内部共享,外部使用者看不到
    pub(crate) fn connection_pool() -> Pool { /* ... */ }

    // 仅 db 模块内部使用
    fn parse_url(url: &str) -> Config { /* ... */ }
}

pub mod models {
    use super::db;

    pub fn list_users() {
        let pool = db::connection_pool(); // ✅ pub(crate),同一 crate 内可访问
        // db::parse_url("...");           // ❌ 私有,编译错误
    }
}

pub(crate) 是实际项目中最常用的分级可见性——把"内部共享但不想对外承诺兼容"的 API 收敛在 crate 边界内,后续重构时不用顾虑外部调用方。

4. 子模块可以访问父模块的私有项 ​

这是 Rust 模块系统与其他语言最大的差异之一:子模块默认可以访问父模块的私有项。Java/C++ 的 private 是"只有本类能访问",而 Rust 的私有边界是模块级别——同一父模块下的子模块之间不能互通,但每个子模块都能看到父模块的私有内容。

rust
mod parent {
    fn private_util() -> String { String::from("辅助信息") } // 父模块私有

    pub mod child_a {
        pub fn call_parent() {
            let info = super::private_util(); // ✅ 子模块通过 super:: 访问父私有函数
            println!("child_a 获取: {info}");
        }
    }

    pub mod child_b {
        pub fn call_parent() {
            // super::super::private_util() 也可以,但没必要——super:: 就是 parent
            let info = super::private_util();
            println!("child_b 获取: {info}");
        }
    }
}

fn main() {
    parent::child_a::call_parent();
    // parent::private_util(); // ❌ 外部 crate 或父模块之外不可访问
}

这一设计允许你把紧密协作的子模块放在同一父模块下,共享父模块的私有辅助代码,同时对外统一暴露精选的公开 API。

五、use 与路径导入 ​

如果不用 use,每次调用都要写完整路径:std::collections::HashMap::new()。`use` 把路径缩短为可直接使用的短名。

1. 基本用法 ​

rust
use std::collections::HashMap;   // 导入 HashMap 本身
use std::io::{self, Read};       // 批量导入:io 模块本身 + Read trait

fn main() {
    let mut map = HashMap::new();
    map.insert("rust", 2024);
}

use std::io::{self, Read} 等价于分别写 use std::io; 和 use std::io::Read——self 在花括号中表示"导入模块本身"。

2. 嵌套路径与 as 重命名 ​

相同前缀的路径用花括号合并,减少重复。名称冲突时用 as 重命名:

rust
use std::collections::{HashMap, HashSet};        // 两个类型同属 collections
use std::io::{self, Read, Write};                // 模块本身 + 两个 trait

fn main() {
    let mut word_count: HashMap<&str, i32> = HashMap::new();
    let mut visited: HashSet<&str> = HashSet::new();
    word_count.insert("hello", 1);
    visited.insert("hello");
    println!("{:?} {:?}", word_count, visited);
}
rust
use std::io::Result as IoResult;   // 避免与 crate 内自定义 Result 冲突
use std::fmt::Result as FmtResult;

fn read_config() -> IoResult<String> {
    Ok(String::from("config loaded"))
}

fn format_config() -> FmtResult {
    use std::fmt::Write;
    let mut s = String::new();
    write!(&mut s, "key=value")?; // write! 宏返回 FmtResult
    Ok(())
}

3. pub use — 重导出 ​

`pub use` 把导入的路径同时公开给外部使用者——你在内部用 use 引入了某类型,外部通过你的 crate 也能直接用。这是构建干净公开 API 的核心工具。

rust
// src/lib.rs
pub mod db {
    mod connection;     // 私有子模块,内部实现细节
    pub use connection::Connection; // 重导出:外部通过 db::Connection 使用
}

mod db {
    pub mod connection {
        pub struct Connection { url: String }
        impl Connection {
            pub fn open(url: &str) -> Self { Connection { url: url.to_string() } }
        }
    }
    // 如果不写 pub use,外部需写 db::connection::Connection
    pub use connection::Connection;
}

// 外部使用者:
// use my_crate::db::Connection;  ← 路径干净,不用关心 connection 子模块

重导出的核心价值:你的内部模块结构可以随意重构(合并、拆分、重命名),只要 pub use 的公开路径不变,外部代码就不受影响。

4. use 的习惯用法 ​

导入类型习惯做法原因
函数use parent::function_name 导入到父模块层级调用时带模块名前缀,一眼看出归属
结构体/枚举use full::Path::to::Struct 导入到当前模块写全路径太啰嗦,直接用短名
Traituse TraitName 导入,或用 as _ 匿名导入trait 方法需要对当前类型生效
同名项as 重命名,或保留父模块前缀避免歧义
rust
// 函数的习惯用法:use 到函数所在模块,调用时带模块前缀
use std::fs;

fn main() {
    let content = fs::read_to_string("config.toml").unwrap(); // 一眼看出是 fs 模块的函数
}

// 结构体的习惯用法:路径全引入,直接用短名
use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new(); // 直接用 HashMap,不用加前缀
}

六、模块拆分实战 ​

1. 拆分信号 ​

不需要一开始就分模块——单文件 200~300 行以内完全够用。以下信号出现时再拆:

  • 自然分组:几个函数都跟用户有关,另几个都是数据库操作——它们已经在概念上形成了边界
  • 导航困难:在同一个文件中频繁翻找函数定义
  • 需要控制可见性:某组函数希望对外的暴露面只有 2 个,其余都是内部实现

2. 可见性设计原则 ​

按照"最小可见"原则逐级放开:

  1. 先全部写为模块私有(不加 pub)
  2. 确认需要被其他模块调用时 → 加 pub(crate)
  3. 确认需要被外部 crate 使用者调用时 → 加 pub

为什么顺序是这样? pub 是 API 承诺——你不小心改了签名,下游就会编译失败。pub(crate) 只影响自己的 crate,重构成本低得多。

3. 常见目录布局 ​

中等规模项目的典型结构:

text
src/
├── main.rs                # 启动入口,依赖注入
├── lib.rs                 # crate root,声明所有顶级模块
├── config.rs              # 配置加载
├── error.rs               # 统一错误类型
├── db/
│   ├── mod.rs             # pub mod models; pub use models::*;
│   ├── models.rs          # 数据模型
│   └── connection.rs      # 数据库连接与连接池
├── services/
│   ├── mod.rs
│   ├── user.rs            # 用户业务逻辑
│   └── order.rs           # 订单业务逻辑
└── handlers/
    ├── mod.rs
    ├── user_handler.rs    # HTTP 请求处理
    └── order_handler.rs
rust
// src/lib.rs —— 声明所有顶级模块
pub mod config;
pub mod error;
pub mod db;
pub mod services;
pub mod handlers;
rust
// src/db/mod.rs —— 数据库模块入口,声明子模块并重导出
mod models;
mod connection;

pub use models::*;         // 外部直接用 db::User,而非 db::models::User
pub use connection::*;

核心规则回顾:每个新文件都需要在它的父级 mod.rs(或 lib.rs / main.rs)中显式 mod 声明——Rust 不会因为文件存在就自动把它加入模块树。

小结 ​

  • Package 是 Cargo.toml 管理的项目单元;Crate 是编译单元,分 binary(需要 main)和 library(需要 lib);Module 用 mod 声明在模块树上
  • 模块树不是文件系统自动生成的——每个子模块必须在父模块中显式 mod 声明。文件存在但没 mod 声明 = 不会被编译
  • 路径导航:crate:: 从根出发(绝对路径,推荐),self:: 同模块(相对路径),super:: 回父模块
  • 默认私有,按需公开:先不加 pub → 需要跨模块加 pub(crate) → 需要对外加 pub。pub use 重导出隐藏内部结构,保持公开 API 干净
  • 子模块可通过 super:: 访问父模块私有项——这点和 Java/C++ 的 private 语义完全不同,是 Rust 模块系统特有的设计
  • 拆分从单文件开始,出现自然分组、导航困难或可见性需求时再拆。先 private、后 pub(crate)、最后 pub 是稳妥的可见性扩放顺序

📖 相关资源

CC BY-NC-SA 4.0 协议