用 mod 控制作用域
本教程共 78 篇 · 第 33 篇 · 更新于 2026-08-08 · 约 8 分钟阅读
本节目标:学会用
mod关键字创建嵌套模块,看懂模块树,并理解 Rust 默认私有、用pub打开可见性的规则。
上一章说了模块是用来组织代码的。这一章动手写,看看模块到底怎么建、作用域怎么划分、为什么你写的函数有时候别人调不到。
1-1 用 mod 创建模块
创建模块很简单,用一个 mod 关键字,后面跟上模块名,再加一对花括号把内容包起来。我们以一个”餐厅”为例,把前厅相关的功能收进一个模块:
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
这段代码干了三件事:
- 用
mod声明模块,名字紧跟其后; - 模块可以层层嵌套,
hosting和serving都嵌套在front_of_house里面,因为接待和服务都发生在前厅; - 模块里能放函数、结构体、枚举、trait 等各种 Rust 类型。
把功能相近的代码放进同一个模块,再用模块名点明它们为什么被归在一起,别人用你的代码时一眼就能懂。
1-2 模块树长什么样
src/main.rs 或 src/lib.rs 叫”包根(crate root)“,它本身就是一个名为 crate 的模块,位于整棵模块树的树根。上面的代码展开后是这样一棵模块树:
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
如果模块 A 包含了模块 B,那么 A 是 B 的父模块,B 是 A 的子模块。这棵树的形状和电脑里的文件夹目录几乎一模一样——不只是长相像,用法也像:文件夹靠路径找文件,Rust 靠路径找模块里的项。
Note模块树这个概念一定要在脑子里立住,因为下一章讲”路径”时,所有路径都是沿着这棵树走的。
1-3 默认私有:为什么调不到
把上面的代码放进库工程编译,你可能会想调用 add_to_waitlist。于是写下:
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist();
}
结果编译器报错:
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ private module
原因在 Rust 的一条铁律:模块里的一切默认都是私有的,包括模块本身。 front_of_house 这个父模块和 eat_at_restaurant 同属于包根作用域,所以父能看见子;但 hosting 是 front_of_house 的私有子模块,包根跨过一层去访问它就被挡住了。
Warning这里有个反直觉的点:Rust 里父模块看不到子模块的私有项,但子模块能看到父模块乃至祖先模块的私有项。方向是”向下封闭、向上开放”。初学者最容易假设父能访问子,恰恰相反。
1-4 用 pub 打开可见性
要让人从外面调到 add_to_waitlist,得把路径上的每一层都打开。先给 hosting 模块加 pub:
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
本以为这下能通,结果又报错:
error[E0603]: function `add_to_waitlist` is private
这就引出一个关键细节:模块的可见性,不等于模块内部项的可见性。 pub mod 只是允许别人”引用到这个模块”,但想用它里面的函数,还得单独把函数也标成 pub。所以最终是:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist();
front_of_house::hosting::add_to_waitlist();
}
Tip为什么不让”公开模块”自动公开全部内部项?因为真实项目里,一个模块真正要对外暴露的往往就寥寥几个。如果公开模块就全公开,你反而得把不想暴露的一个个藏起来,更麻烦。Rust 选择”最小暴露”,更省心也更安全。
1-5 结构体和枚举的可见性差异
把 pub 用在结构体和枚举上,表现不一样,值得单独记:
- 结构体标了
pub,它的字段仍然默认私有,要哪个字段公开就单独给哪个字段加pub; - 枚举标了
pub,它的所有变体自动公开。
原因很实际:枚举如果变体看不见,整个枚举就没法用,所以 Rust 让它跟着枚举一起公开;而结构体的字段常常分散在不同地方使用,编译器没法替你决定哪些该公开,干脆默认都关上,交给你自己选。
pub struct Breakfast {
pub toast: String, // 这个字段公开
seasonal_fruit: String, // 这个字段仍私有
}
pub enum Appetizer {
Soup, // 自动公开
Salad, // 自动公开
}
1-6 pub 只打开“直接这一层”
pub 的语义经常让新手误会:它只把”紧挨着的那一层”打开,并不会一路通到底。
举例:模块 a 里有个 pub mod b,b 里有个函数 c 却没写 pub。那么从 a 外面你能 use a::b,但 a::b::c 依然不可见——因为 c 自己还是私有的。pub 像一扇一扇的门,每个想被外部访问的项都得自己有这扇门。
这也解释了为什么结构体字段要单独标 pub:即使整个结构体是 pub,它的字段默认仍是私有的,外部读不到。枚举则相反,给 enum 标 pub 后,所有变体自动对外可见,不需要逐个标。
Warning常见错误:在父模块写了
pub mod,就以为子模块里所有东西都暴露了。记住,可见性是”逐项”的,每一层都要自己放行。
如果想把某个项开放给”整个 crate 内部”但对外隐藏,用 pub(crate);想精确到某个路径,用 pub(in crate::某路径)。这两种受限可见性在写库时非常有用:既能让同库的其他模块协作,又不把实现细节泄露给使用者。
1-7 一个综合例子:从零搭带层级的模块
光说规则有点抽象,来看一个完整的迷你例子。假设我们要做一个”几何”模块,里面有”二维”和”三维”两个子模块,并希望外部能用到二维里的点,但三维暂时只给自己用。
mod geometry {
pub mod dim2 {
pub fn point() { println!("二维点"); }
}
mod dim3 { // 没有 pub,外部进不来
pub fn cube() { println!("立方体"); }
}
}
这里 geometry 没标 pub,所以外部连 geometry 都看不到;dim2 标了 pub,外部能 use 到它;dim2 里的 point 也标了 pub,所以 crate::geometry::dim2::point 全程可达。而 dim3 整个私有,连它的 cube 写没写 pub 都无关紧要——父模块都没开放,子模块再开放也没用。
Tip判断”某个东西能不能被外部访问”,从 crate 根开始一层层往下看:每一层都得有
pub才能继续往下走。哪一层断了,后面的就全不可见。这是排查”为啥调不到”的最快办法。
1-8 小结
这一章的几个要点:
mod 名字 { ... }创建模块,模块可以嵌套,形成模块树;- 包根
src/main.rs/src/lib.rs就是名为crate的树根; - Rust 默认一切私有,父看不见子的私有项,子能看见父的私有项;
- 想对外暴露,就给”模块”和”模块内部的项”都加
pub; - 结构体和枚举的
pub行为不同,枚举变体跟着公开,结构体字段不跟着。
路径怎么写、怎么用 use 省去长串前缀,下一章接着讲。