Alex

彻底搞懂 Rust 模块系统:它不是文件系统,而是一棵树

很多刚接触 Rust 的开发者,都会在模块系统这里卡一下。

你明明在 src/ 下新建了一个 cpu.rs,结果在 main.rs 里却怎么都引用不到。
如果你有 Java、C++ 或 Go 背景,这种困惑尤其常见,因为我们很容易下意识地认为:

“文件放进目录里了,编译器应该自己知道吧?”

但 Rust 不是这么工作的。

Rust 的模块系统,本质上不是文件系统映射,而是一棵由你手动声明出来的模块树(module tree)。文件只是模块内容的一种承载方式,不是模块关系本身。

理解这一点,Rust 模块系统就会一下子清晰很多。


一句话先讲透:Rust 不会自动扫描你的目录

在 Rust 中,一个项目首先是一个 crate。而一个 crate 的内部结构,不是“哪个文件夹里有什么文件”,而是:

从 crate root 开始,逐层显式声明出来的一棵模块树。

通常:

  • src/main.rs 是二进制 crate 的根(crate root)
  • src/lib.rs 是库 crate 的根(crate root)

从这个根开始,Rust 只认你明确声明过的模块。

也就是说:

  • 你创建了 cpu.rs不等于 Rust 自动拥有了 cpu 模块
  • 你必须先在父模块中写 mod cpu;
  • 这样 Rust 才会把这个文件挂到模块树上

所以 Rust 模块系统最重要的心法是:

先挂载,后使用。


moduse 到底分别干什么?

很多人会把这两个关键字混在一起,其实它们职责完全不同。

mod:声明模块,把树枝接上

mod 的作用,是定义模块结构

例如:

mod constants;

这句话的含义不是“导入 constants”,而是:

告诉编译器:当前模块下面有一个叫 constants 的子模块,请去对应文件里找它。

它相当于在模块树上接了一根树枝。

另一种写法是内联模块:

mod cpu {
    fn run() {}
}

这表示直接在当前文件里定义一个子模块,而不是去外部文件查找。

use:引用模块内容,创建快捷方式

use 并不会创建模块,它只是为了让路径写起来更方便。

例如:

use crate::constants::MEMORY_SIZE;

这句话的前提是:constants 模块必须已经通过 mod constants; 被挂到树上了。

可以记住这句很好用的类比:

mod 是接电线(没它没电),use 是接插座(为了好用)。


Rust 2018 之后,推荐怎样组织模块?

早期 Rust 很多项目喜欢用 mod.rs,但从 Rust 2018 Edition 开始,更主流、更清晰的组织方式是:

同名文件 + 同名目录

例如:

src/
├── main.rs
├── hardware.rs
└── hardware/
    └── cpu.rs

这时模块关系通常是这样的:

main.rs 中:

mod hardware;

hardware.rs 中:

pub mod cpu;

这样就形成了模块树:

crate
└── hardware
    └── cpu

这里有个关键点:

  • hardware.rsmain.rs 的子模块
  • 同时它又是 hardware/cpu.rs 的父模块

也就是说,文件系统只是帮助你组织代码;真正决定模块关系的,永远是 mod 声明本身。


跨模块访问,本质上是在树上找路径

当你在 cpu.rs 里想访问别处的内容时,本质上是在模块树里“导航”。

Rust 常见有三种导航方式。

绝对路径:crate::...(最稳)

从 crate 根节点开始找:

use crate::constants::MEMORY_SIZE;

这是最推荐新手优先使用的方式,因为它最稳健:

  • 不依赖当前文件所在位置
  • 重构时更不容易出错
  • 路径语义更清晰

相对路径:super::...(去上一层)

super 表示当前模块的父模块:

use super::constants;

它适合表达“我在当前模块附近找兄弟节点”,但层级复杂时可读性不如 crate::

当前模块路径:self::...(在自己房间找)

self 表示当前模块自己:

use self::helper::parse;

它常用于模块内部组织代码,让路径显式一些。


真正常见的坑:不是路径,而是可见性(pub

很多“找不到”的问题,其实并不是模块没声明,而是权限没打开

Rust 模块系统默认遵循一个原则:

默认私有(private by default)

这和很多语言的默认习惯不同,所以容易踩坑。

模块默认是私有的

mod cpu;

表示 cpu 是当前模块的私有子模块,外部不能直接访问。

如果希望别的模块也能访问它,需要:

pub mod cpu;

模块里的成员也默认私有

即便模块本身公开了,里面的函数、结构体、常量如果没写 pub,外面一样不能用:

pub mod cpu {
    pub fn run() {}
}

pub(crate):对整个当前 crate 可见

这是 Rust 里非常实用的可见性控制:

pub(crate) mod cpu;

含义是:

  • crate 内部随便用
  • crate 外部不可见(不会成为公共 API)

“套娃效应”:父模块不开门,子模块 pub 也没用

可见性是逐层生效的。路径上的每一层都必须“打通”。

就像你把卧室门开着,但如果家门锁了,外人仍然进不来。


新手最实用的排错 Checklist

如果你遇到这些错误:

  • 找不到模块 / unresolved import
  • item is private / module is private

按这个顺序排查,通常能很快定位问题:

  1. 挂载检查:在 main.rs / lib.rs 或父模块里写 mod xxx; / pub mod xxx; 了吗?
  2. 父模块可见性:要走的路径上,每一层父模块都对你可见吗?(父层私有会导致子层 pub 也无效)
  3. 成员可见性struct / fn / const 等具体成员加 pub 了吗?
  4. 路径检查:新手建议优先用 crate:: 绝对路径,少绕弯子、也更抗重构。

结语:接受“手动建树”,模块系统就顺了

Rust 的模块系统起初可能觉得繁琐,但一旦你接受了“手动建树”的设定,你会发现它带来的重构安全性访问控制是极其强大的。

最后用一句话把它钉死:

Rust 的模块系统不是“文件自动组成目录结构”,而是“你通过 mod 显式搭建出来的一棵树”。