将模块拆分到多文件
本教程共 78 篇 · 第 35 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:把塞在一个文件里的模块拆到独立 .rs 文件,弄懂
mod 名字;(分号)如何从外部文件加载模块,以及文件夹当模块时的两种写法。
前几章的模块都写在 src/lib.rs 或 src/main.rs 里,全部挤在一个文件。工程小的时候没问题,可一旦代码变多,单文件会膨胀得没法看:几千行堆在一起,想找个函数得翻半天,改一处还怕动到别的逻辑。
这一章只解决一件事:把模块搬到各自的文件里,让工程更好维护。拆完之后你会发现,代码逻辑一点没变,只是物理位置换了,阅读和维护体验天差地别。
1-1 从”声明”与”实现”分离说起
先回看原来挤在一起的样子。在 src/lib.rs 里,模块和它的内容写在一块:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
现在把 front_of_house 的内容搬出去。先在 src/ 下新建 front_of_house.rs,只放模块自己:
pub mod hosting {
pub fn add_to_waitlist() {}
}
src/lib.rs 里就只剩”声明”和”调用”:
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
注意这一行 mod front_of_house;,是分号结尾,没有花括号。它告诉 Rust:去加载一个和模块同名的文件,把里面的内容作为这个模块。这就是声明与实现分离——lib.rs 里只是”声明有这个模块”,真正的代码在 front_of_house.rs 里。
Note关键认知:模块
front_of_house的”定义”(入口)依然在src/lib.rs(那句mod front_of_house;),只是模块的内容被搬到了src/front_of_house.rs。编译器仍以lib.rs里的声明为入口去找文件,并不会因为内容搬走就找不到模块。
1-2 拆文件后,路径写法完全不变
很多人担心:文件一拆,原来写的 crate::front_of_house::hosting 路径是不是要改?答案是不用改。
因为对编译器来说,模块树的逻辑结构跟之前一模一样,只是代码的物理位置换了。文件只是模块的”容器”,模块树才是真相。你随便怎么拆文件,只要 mod 声明对得上,路径就稳定。
这一点在工程里非常值钱:你可以随心所欲地重组文件布局(甚至把整个模块目录挪来挪去),而不用动任何一处调用代码。这也是 Rust 模块系统”逻辑与物理分离”设计的好处。
1-3 文件夹当模块:两种写法
当一个模块下面还有好多子模块(比如前厅里有招待、有上菜),继续平铺成一堆 .rs 文件会乱。这时可以用文件夹来表示模块。比如把 front_of_house 变成文件夹,里面放子模块 hosting.rs:
// src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}
配套地,src/lib.rs 里还是 mod front_of_house;。但编译会报错:
error[E0583]: file not found for module `front_of_house`
--> src/lib.rs:3:1
|
1 | mod front_of_house;
| ^^^^^^^^^^^^^^^^^^
|
= help: to create the module `front_of_house`, create file
"src/front_of_house.rs" or "src/front_of_house/mod.rs"
报错说得很直白:要把一个文件夹当成模块,必须再有一个文件告诉 Rust 这个文件夹里有哪些子模块。有两种做法:
做法一:建 src/front_of_house/mod.rs(旧版 Rust 1.30 之前唯一的方式):
// src/front_of_house/mod.rs
pub mod hosting;
做法二:建 src/front_of_house.rs(和文件夹同名、放在它的同级目录):
// src/front_of_house.rs
pub mod hosting;
两种写法文件内容一样,都是声明子模块。最终目录结构是这样:
src
├── front_of_house
│ └── hosting.rs
├── front_of_house.rs # 或改用 front_of_house/mod.rs
└── lib.rs
Tip新版本官方更推荐”同名文件”写法(
front_of_house.rs),避免项目里出现一堆同名mod.rs让人分不清属于哪个模块。但mod.rs在老代码和某些工具里仍常见,两种你都得认得,读别人工程时别懵。
1-4 入口文件只负责”挂”子模块
无论用哪种写法,那个”入口文件”(mod.rs 或 front_of_house.rs)只负责把子模块挂上来:
pub mod hosting;
// pub mod serving;
而真正的函数实现在 src/front_of_house/hosting.rs 里。一层层这么拆下去,再大的工程也能保持清爽:每个文件只关心自己那一块,找代码时顺着文件夹结构走就行,像翻目录一样自然。
Warning初学者常犯一个错:把
mod front_of_house { ... }写在lib.rs,又同时存在front_of_house.rs文件,以为两者会合并。不会。两者的内容不会自动合并,反而可能冲突或找不到文件。要么在lib.rs里用花括号内联写模块,要么用mod front_of_house;指向外部文件,二选一,别混用。
1-5 一个稍完整的例子
把餐厅的前厅、后厨都拆开,目录长这样:
src
├── lib.rs
├── front_of_house.rs
├── front_of_house
│ ├── hosting.rs
│ └── serving.rs
├── back_of_house.rs
└── back_of_house
└── cook.rs
lib.rs 只做声明和对外再导出:
mod front_of_house;
mod back_of_house;
pub use crate::front_of_house::hosting;
pub use crate::back_of_house::cook_order;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
cook_order();
}
子模块文件各自安安静静地放自己的实现。这样的结构,比把所有代码堆在一个文件里好读、好改、好协作。
1-6 拆分的三条主线
把零散要点收一下,拆文件其实就这三件事:
- 声明在外、实现在内:
mod 名字;(分号)从同名文件加载模块,实现与声明分离; - 文件夹当模块要加入口:需要
mod.rs或同级的front_of_house.rs来声明子模块; - 路径不受影响:模块树逻辑结构不变,调用代码一行都不用改。
Note记住一个心智模型:模块树是”逻辑真相”,文件目录是”物理容器”。你重组容器时,只要
mod声明还在,逻辑真相就不动,调用方也就不受影响。
1-7 内联还是拆文件:怎么选
新手常纠结:到底什么时候该把模块拆出去?给你一条简单标准——当一个模块的内容超过一屏(大约 50 行),或者它和同文件里的其他模块没什么关系时,就该拆。
反过来,如果一个模块只有两三个小函数、还跟当前文件强相关,留在原地反而更方便,硬拆反而要多开一个文件、多写一句声明,反而碍事。所以别为了”看起来规范”而过度拆分,按需来就好。
还有一点容易忽略:pub use 再导出是定义在入口文件(如 lib.rs)里的。它把深层路径”缩短”成对外友好的名字。比如用户只要写 use restaurant::hosting; 就能拿到 add_to_waitlist,不用关心它藏在 front_of_house 几层之下。拆文件时,这种再导出语句通常留在最外层入口,不要跟着实现一起搬进子文件。
1-8 小结
把零散要点收一下,拆文件其实就这三件事:
- 声明在外、实现在内:
mod 名字;(分号)从同名文件加载模块,实现与声明分离; - 文件夹当模块要加入口:需要
mod.rs或同级的front_of_house.rs来声明子模块; - 路径不受影响:模块树逻辑结构不变,调用代码一行都不用改。
Tip拆文件不是目的,可读性才是。每次拆完,不妨从”一个陌生同事来读”的角度看一眼目录:能不能顺着文件夹就猜到代码在哪?能,就拆对了。
到这里,模块系统的组织、作用域、路径、拆分都讲完了。下面进入”集合类型”——先看最常用的 Vec。