首页
/ Rust 模块级封装:结构体字段私有性与可见性控制(Comprehensive Rust 课程精讲)

Rust 模块级封装:结构体字段私有性与可见性控制(Comprehensive Rust 课程精讲)

2026-09-09 19:58:12作者:鲍丁臣Ursa

本文基于 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 库单模块实现中,LabelButtonWindow 的字段全部私有,统一通过 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();
}

这个练习清晰呈现了三条规则的协同:

  1. widgets/button.rs 等子模块与 widgets.rs 属于同一模块族,因此可以访问彼此的私有细节(如 Button 内部的 Label 类型);
  2. main.rs 位于模块之外,只能接触 pub 重导出的类型与公开方法;
  3. 各 widget 的内部布局(字段)完全私有,未来重构内部实现不会破坏 main.rs

配套的本地工程配置可参考 src/modules/Cargo.tomledition = "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 引入系统开发时最看重的能力之一——类型系统的安全承诺,从模块边界开始。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.16 K
2.78 K
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
904
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
932
1.86 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
862
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.95 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.38 K
1.47 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
535
606
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
549
398
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Markdown
77
23