Rust 模块级封装:结构体字段私有性与可见性控制(Comprehensive Rust 课程精讲)
本文基于 Google Android 团队维护的 Rust 课程(Comprehensive Rust)中 src/modules/encapsulation.md 一章展开。文章以"模块即隐私边界"为主线,系统讲解 Rust 结构体字段默认私有的规则、模块内(含子模块)的可见性边界、通过构造器维护不变量实现封装,以及枚举在隐私上的特殊性,并结合本仓库的源码与配套练习给出可运行的完整示例。
导读
在 Rust 中,隐私(privacy)的边界是模块,而不是类型——这与 Java、C++ 等以"类"为封装单位的面向对象语言截然不同。本篇技术指南将带你理解:为什么结构体字段默认私有、私有字段在模块及子模块中的可见范围如何划定,以及如何利用这些规则把实现细节关进"模块的笼子"里,从而在不暴露内部状态的前提下提供安全的公开 API。读完本文,你将掌握 Rust 模块级封装的核心规则、不变量维护的惯用写法,以及枚举在隐私语义上的特殊之处(含 #[non_exhaustive] 等替代控制手段),并能在真实 Cargo 项目中自如运用。
一、一切始于"模块即隐私边界"
在深入结构体之前,先建立最底层的心智模型:模块(module)是 Rust 中唯一的隐私边界。正如 src/modules/visibility.md 所总结的:
- 模块内的条目(函数、结构体、字段、子模块等)默认都是私有的,用于隐藏实现细节;
- 父模块和兄弟模块中的条目总是可见的;
- 换一种说法:如果一个条目在模块
foo中可见,那么它在foo的所有后代模块(descendants)中也可见。
mod outer {
fn private() {
println!("outer::private");
}
pub fn public() {
println!("outer::public");
}
mod inner {
fn private() {
println!("outer::inner::private");
}
pub fn public() {
println!("outer::inner::public");
super::private(); // 子模块可以访问父模块的私有条目
}
}
}
fn main() {
outer::public();
}
可以看到,outer::inner::public 中通过 super::private() 调用了父模块的私有函数——因为"可见性"向下游传递:条目在 outer 中可见,就在 outer 的所有后代模块中可见。这正是 src/modules/modules.md 中"模块定义组织、作用域"的延伸:模块不仅是命名空间,更是访问控制的闸门。更精细的可见性控制(如 pub(crate)、pub(in path))也完全建立在"模块是隐私边界"这一基础之上。
二、结构体字段默认私有:与模块条目一致的规则
结构体字段的可见性与模块条目遵循完全相同的规则:默认私有。 私有字段在所属模块的其余部分(包括子模块)中依然可见。这使得我们可以封装结构体的实现细节,精确控制哪些数据和功能对外暴露。
下面是与课程配套的完整示例(见 src/modules/encapsulation.md,可直接在 Rust Playground 或本地运行):
use outer::Foo;
mod outer {
pub struct Foo {
pub val: i32,
is_big: bool, // 私有字段,模块外不可见
}
impl Foo {
pub fn new(val: i32) -> Self {
Self { val, is_big: val > 100 }
}
}
pub mod inner {
use super::Foo;
pub fn print_foo(foo: &Foo) {
println!("Is {} big? {}", foo.val, foo.is_big);
}
}
}
fn main() {
let foo = Foo::new(42);
println!("foo.val = {}", foo.val);
// let foo = Foo { val: 42, is_big: true }; // 错误:不能初始化含有私有字段的结构体
outer::inner::print_foo(&foo);
// println!("Is {} big? {}", foo.val, foo.is_big); // 错误:is_big 是私有字段,外部不可直接访问
}
这个例子浓缩了全部核心规则,值得逐行拆解:
| 代码片段 | 可见性分析 |
|---|---|
pub struct Foo |
结构体本身对外公开,外部可以通过路径 outer::Foo 引用它 |
pub val: i32 |
字段公开,模块外可读可写(foo.val 合法) |
is_big: bool |
字段私有,模块外不可访问 |
impl Foo { pub fn new(...) } |
公开构造器,是外部创建 Foo 实例的唯一合法途径 |
mod inner 中的 print_foo |
子模块属于 outer 的可见性范围,可以读取 is_big |
三、模块内的可见范围:兄弟与子模块
一个关键细节是:私有字段的"可见域"不是"类型内部",而是"模块内部(含其子模块)"。也就是说,只要代码位于定义该结构体的模块(或它的任何后代模块)中,就可以读写私有字段,哪怕这段代码与结构体定义在不同文件里。
这正是课程中反复强调的、与面向对象语言最大的差异点:
- 在 Java / C++ 中,封装边界是类——只有类自身(以及友元/嵌套类等特例)能访问
private成员; - 在 Rust 中,封装边界是模块——同模块下的多个类型、函数、
impl块共享彼此的私有字段。
因此,辅助函数(helper functions)可以定义在与结构体相同的模块(包括子模块)中,从而合法访问该类型的私有字段与方法。示例中的 outer::inner::print_foo 就是教科书式的"模块内辅助函数":它不属于 Foo 的方法,却因为身处 Foo 的模块族内而能读取 foo.is_big。
这种设计让"能力"与"数据"可以分离组织:公开 API 保持精简,而内部工具函数可以自由分布在模块树的各处。注意,模块隐私规则在 impl 块位于其他模块时依然生效——即你不能通过把 impl 挪到别处来"偷看"私有字段,这一点在课程的 "More to Explore" 中有专门演示(见 src/modules/encapsulation.md 末尾)。
四、封装的真正价值:构造器与不变量(Invariants)
把 is_big 声明为私有,绝非仅仅为了"藏起来",而是为了让类型全权控制自己的状态。观察示例中的构造器:
impl Foo {
pub fn new(val: i32) -> Self {
Self { val, is_big: val > 100 }
}
}
is_big 的初始化逻辑完全由 Foo 掌控,从而能够强制执行它需要的不变量——例如:is_big 只有在 val > 100 时才为 true。
如果 is_big 是公开字段,外部调用者就可以写出 Foo { val: 5, is_big: true } 这种自相矛盾的状态,破坏类型的不变量;而一旦字段私有,外部只能通过构造器 Foo::new(val) 创建实例,val > 100 这一约束就得到了结构性的保障。这是 Rust 社区中"parse, don't validate"(解析而非校验)思想的基础:与其在每次使用时校验状态,不如用私有字段 + 受控构造器让非法状态根本无法被构造出来。
课程的配套练习进一步印证了这一模式。在 src/modules/exercise.rs 的 GUI 库单模块实现中,Label、Button、Window 的字段全部私有,统一通过 new 构造器初始化:
pub struct Window {
title: String,
widgets: Vec<Box<dyn Widget>>,
}
impl Window {
fn new(title: &str) -> Window {
Window { title: title.to_owned(), widgets: Vec::new() }
}
}
外部代码无法绕过 add_widget 直接往 widgets 里塞数据,窗口的状态变迁路径因而完全可控。
五、私有字段带来的两个硬性限制
课程在示例代码中特意留下了两行被注释掉的代码,分别对应私有字段引发的两类编译错误:
限制一:不能直接初始化含有私有字段的结构体
// let foo = Foo { val: 42, is_big: true }; // ❌ 编译错误
即使 val 是公开字段,只要结构体里存在任何私有字段,外部就无法使用结构体字面量语法整体构造它。这正是"唯一合法创建路径是 Foo::new"的强制力来源。
限制二:不能直接访问私有字段
// println!("Is {} big? {}", foo.val, foo.is_big); // ❌ 编译错误:is_big 为私有
foo.val 合法(公开字段),foo.is_big 非法(私有字段)。这类错误由借用检查器之外的隐私检查在编译期拦截,属于 Rust 编译器的静态保证之一。
对于公开字段,你还可以使用 pub(crate) 等受限可见性标注,将字段的可见范围压缩到 crate 内部。以 src/modules/paths.md 中介绍的路径体系配合使用,可精确控制每一层的暴露程度。
六、枚举的特殊性:变体永远是公开的
课程特别提醒:枚举(enum)不支持隐私——其变体(variants)以及变体内携带的数据永远是公开的。 也就是说,pub enum 的变体对外部一律可见,不存在"私有变体"一说。
这带来一个实际困扰:一旦枚举定义被公开,新增或重命名变体就可能破坏下游代码的穷尽匹配(exhaustive match)。课程在 "More to Explore" 中给出的应对手段是:
#[non_exhaustive]:标注在枚举(或结构体)上,提示外部使用者"此类型未来可能增加变体/字段",强制外部匹配必须带有兜底分支(_ => ...),从而为库作者保留向后演进的余地;#[doc_hidden]:从 rustdoc 生成的 API 文档中隐藏某些条目,弱化它们被外部使用的"表面可见性"。
#[non_exhaustive]
pub enum Message {
Text(String),
Image { url: String, width: u32, height: u32 },
// 未来可能新增变体,外部匹配必须加兜底分支
}
#[doc_hidden]
pub fn internal_helper() { /* 文档中不可见,但技术上仍可调用 */ }
这两个属性并非真正的隐私机制(技术上外部依然可以访问),但它们是 Rust 生态中"限制枚举外部操作面"的惯用工具,用来弥补枚举天生无法私有化变体的缺憾。
七、实战:用模块重构 GUI 库,让封装落地
理解了规则,还要能在真实项目中组合运用。课程的模块章节配套练习(src/modules/exercise.md)要求把单文件 GUI 库拆分为多模块,其过程正是封装规则的集中演练:
cargo init gui-modules
cd gui-modules
cargo run
推荐的目录划分(完整答案见 src/modules/solution.md):
src
├── main.rs
├── widgets
│ ├── button.rs
│ ├── label.rs
│ └── window.rs
└── widgets.rs
其中 src/widgets.rs 作为模块的"门面",集中声明并重导出公开类型:
// ---- src/widgets.rs ----
pub use button::Button;
pub use label::Label;
pub use window::Window;
mod button;
mod label;
mod window;
pub trait Widget {
fn width(&self) -> usize;
fn draw_into(&self, buffer: &mut dyn std::fmt::Write);
fn draw(&self) { /* 默认实现 */ }
}
子模块(如 label.rs 对应逻辑)则通过 use super::Widget; 引用父模块的 trait,各类型字段保持私有、以 pub fn new 构造:
// ---- src/widgets/button.rs ----
use super::{Label, Widget};
pub struct Button {
label: Label, // 私有字段,外部只能通过 new 构造
}
impl Button {
pub fn new(label: &str) -> Button {
Button { label: Label::new(label) }
}
}
main.rs 只需一条路径即可消费整个库:
// ---- src/main.rs ----
mod widgets;
use widgets::{Button, Label, Widget, Window};
fn main() {
let mut window = Window::new("Rust GUI Demo 1.23");
window.add_widget(Box::new(Label::new("This is a small text GUI demo.")));
window.add_widget(Box::new(Button::new("Click me!")));
window.draw();
}
这个练习清晰呈现了三条规则的协同:
widgets/button.rs等子模块与widgets.rs属于同一模块族,因此可以访问彼此的私有细节(如Button内部的Label类型);main.rs位于模块之外,只能接触pub重导出的类型与公开方法;- 各 widget 的内部布局(字段)完全私有,未来重构内部实现不会破坏
main.rs。
配套的本地工程配置可参考 src/modules/Cargo.toml(edition = "2024",二进制入口指向 exercise.rs)。若想进一步了解模块在文件系统层面的映射规则(如 mod garden; 对应 src/garden.rs、#[path] 指令等),可继续阅读 src/modules/filesystem.md。
八、小结:封装决策速查表
| 场景 | 规则 / 做法 |
|---|---|
| 结构体字段默认可见性 | 私有;仅在定义它的模块及其子模块内可见 |
| 需要对外暴露字段 | 标注 pub;如需限制范围可用 pub(crate)、pub(in path) |
| 外部如何构造含私有字段的结构体 | 通过 pub fn new(...) 之类的公开构造器/工厂函数 |
| 如何维护类型不变量 | 私有化敏感字段,把校验收敛进构造器(如 is_big: val > 100) |
| 想在模块外提供内部辅助逻辑 | 把辅助函数放进同模块或子模块,天然获得私有字段访问权 |
| 枚举变体 | 永远公开,无法私有化;用 #[non_exhaustive] 保留演进空间 |
| 不想让条目出现在 API 文档 | 使用 #[doc_hidden] |
理解"模块是唯一的隐私边界,可见性向下游传递"这一句,就掌握了 Rust 封装的钥匙:它既不同于类的私有性,又比任何语言都更彻底地让"不可变状态 + 受控入口"成为编译器可验证的保证。这正是 Android 团队将 Rust 引入系统开发时最看重的能力之一——类型系统的安全承诺,从模块边界开始。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python270
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python46066
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go20143
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java34051