本节摘要:模块把 crate 内部划分为命名空间与可见性边界,默认私有、pub 才对外。本节讲 mod 声明、模块树、路径引用与 pub 的各种粒度。读完你能把单文件项目拆成清晰的多模块结构。
mod archive { pub struct Case { pub no: u32, // 字段也要逐个公开 sealed: bool, // 私有:外界不可见、不可构造 } impl Case { pub fn new(no: u32) -> Self { Case { no, sealed: false } } pub fn seal(&mut self) { self.sealed = true; } } } let mut c = archive::Case::new(173); // 用路径访问 c.seal();
默认私有的收益:内部实现可以随意重构,只要 pub 面不变。Case 有私有字段意味着外界无法绕过构造函数制造非法状态——第 4.2 节"让非法状态不可表示"的工程落地。
mod 可以嵌套、可以搬到独立文件(目录与同名文件两种组织法都行)。路径有绝对(crate 开头)与相对(self、super 开头)两种;use 把常用路径引入作用域,as 可重命名避免撞名。pub 的粒度还有细化版:pub(crate) 对整个 crate 可见——库内部共享但不进公共 API 的标准写法。

同一棵模块树可以对应多种文件摆法,编译器只认"声明与目录约定",认清楚就能自由选择。
// src/main.rs —— 根模块 mod evidence; // 对应 src/evidence.rs 或 src/evidence/mod.rs fn main() { let id = evidence::new_id(); println!("编号 {}", id); archive::file(&id.to_string()); } mod archive { // 内联模块:小工具就近安家 pub fn file(name: &str) { println!("归档 {}", name); } }
// src/evidence.rs pub(crate) fn new_id() -> u32 { 173 } // 仅本 crate 可见 pub fn public_api() {} // 完全公开 pub(super) fn helper() {} // 只对父模块公开(此处父是根)
可见性修饰是三个档次而非两档,工程价值巨大:pub(crate) 是内部协作的默认档,pub(super) 用于只向直属上级负责的工具,全 pub 只留给真正的对外承诺。审阅一个 crate 的 API 面积,数一数真正的 pub 就够了。
use std::collections::HashMap; // 常规引入 use std::fmt::{self, Display}; // 组合引入 use std::io::Result as IoResult; // 改名避开冲突 pub use crate::evidence::new_id as issue_id; // 重导出:私路径公开化
重导出 pub use 是库的门面手法:内部按层分目录,对外用根模块统一陈列,调用方不必感知内部结构。标准库的 prelude 就是这一手法集大成者。
edition 2018 起外部路径必须从 crate 名或 use 引入起步,本地路径以 crate::、self::、super:: 开头。use evidence::X 在旧判例集(2015)里指本地模块,在新集里是外部包,迁移老代码时这是最高频的错位点,报错是 E0432 "unresolved import",修法是补 crate:: 前缀。
判例一:工具函数从私有到公开的迁移路径。先 pub(crate) 服务内部,出现真实外部需求才升 pub——先内后外的顺序保证 API 面积不虚胖。判例二:深层嵌套的治理。三层以上的 mod 嵌套通常意味着职责不清,用 re-export 把常用项抬到浅层,比让调用方写长路径更体面。判例三:测试的归属。能测私有细节的单元测试放 #[cfg(test)] 同文件;跨模块行为的验证放 tests/ 目录,从外部视角审阅——两席分工好,"测试逼着改可见性"的坏味道自然消失。
// src/lib.rs —— 一个典型的浅门面 mod internal { pub(crate) fn checksum(data: &[u8]) -> u16 { data.iter().fold(0, |a, b| a + *b as u16) } } pub use internal::checksum as public_checksum; // 门面重导出:内部深,外部浅
判例三的补充:#[cfg(test)] mod tests 写在被测文件底部是社区主流,隔文件放 tests.rs 也合法,团队统一即可,真正不可妥协的是"集成测试只走公开 API"这条边界。
把一个膨胀的 main.rs 拆成模块树,标准动作六步:新建 src/lib.rs 承接可复用逻辑;按职责切出一级模块(如 evidence、archive);main.rs 只留参数解析与调度;跨模块引用统一走 use 路径并清理未用项;为每个模块补一句 doc 注释说明管辖权;跑全量测试确认零回归。走查中最容易翻车的是第四步——pub 档位拿捏:先全 pub(crate) 让编译通过,再逐个降级到最小可见性,编译器会用 E0624 一类报错指出哪里降过头。这个"先通后严"的次序比一次到位省时得多。
// 拆分后的 main.rs 应当短到可以整屏看完 use demo::evidence; use demo::archive; fn main() { let args: Vec<String> = std::env::args().skip(1).collect(); let id = evidence::parse_id(args.first()).expect("需要案号参数"); archive::file(&format!("卷宗{}", id)); }