第 22 条:最小化可见性
Rust 允许用户控制一段代码中的元素对其他代码而言是隐藏还是可见的。本条目探索可见性机制,以及给出选择合适可见性的一些建议。
可见性的语法
模块(module)是 Rust 中控制可见性的基本单元。默认情况下,一个模块中的条目(类型、方法、常量等)是私有的,只能被本模块以及其子模块中的代码访问。
使用 pub
关键字,可以扩大代码的可见范围。对于大部分 Rust 语法特性来说,将某个条目标记为 pub
并不会公开其代码内容,例如:pub mod
中的类型和函数不是公开的,pub struct
的字段也不是公开的。但是,也有一些例外情况,将可见性应用到类型的内容中也是合理的:
- 一个标记为公开的
enum
,它所包含的枚举变体以及变体所包含的字段也是公开的。 - 一个标记为公开的 trait,它的所有方法都是公开的。
所以,一个模块中的类型:
#![allow(unused)] fn main() { pub mod somemodule { // 将 `struct` 标记为公开,不会自动将其字段公开。 #[derive(Debug, Default)] pub struct AStruct { // 默认情况下,字段是私有的 count: i32, // 必须显式增加 `pub` 以使得字段对外可见 pub name: String, } // 类似的,结构体中的方法也需要 `pub` 标记使其对外可见 impl AStruct { // 默认情况下,方法也是私有的。 fn canonical_name(&self) -> String { self.name.to_lowercase() } // 必须显式增加`pub` 让其对外可见 pub fn id(&self) -> String { format!("{}-{}", self.canonical_name(), self.count) } } // 标注为公开的 `enum` 它所包含的变体都是对外可见的 #[derive(Debug)] pub enum AnEnum { VariantOne, // 以及变体中的字段也是对外可见的 VariantTwo(u32), VariantThree { name: String, value: String }, } // 标记为公开的 `trait`,它的方法都是对外可见的 pub trait DoSomething { fn do_something(&self, arg: i32); } } }
允许外部代码访问模块中标记为 pub
的条目,以及,前述提及的例外情况(自动对外可见):
#![allow(unused)] fn main() { use somemodule::*; let mut s = AStruct::default(); s.name = "Miles".to_string(); println!("s = {:?}, name='{}', id={}", s, s.name, s.id()); let e = AnEnum::VariantTwo(42); println!("e = {e:?}"); #[derive(Default)] pub struct DoesSomething; impl DoSomething for DoesSomething { fn do_something(&self, _arg: i32) {} } let d = DoesSomething::default(); d.do_something(42); }
但是,未标记为 pub
的,则不可被外部访问:
#![allow(unused)] fn main() { let mut s = AStruct::default(); s.name = "Miles".to_string(); println!("(inaccessible) s.count={}", s.count); println!("(inaccessible) s.canonical_name()={}", s.canonical_name()); }
error[E0616]: field `count` of struct `somemodule::AStruct` is private
--> src/main.rs:230:45
|
230 | println!("(inaccessible) s.count={}", s.count);
| ^^^^^ private field
error[E0624]: method `canonical_name` is private
--> src/main.rs:231:56
|
86 | fn canonical_name(&self) -> String {
| ---------------------------------- private method defined here
...
231 | println!("(inaccessible) s.canonical_name()={}", s.canonical_name());
| private method ^^^^^^^^^^^^^^
Some errors have detailed explanations: E0616, E0624.
For more information about an error, try `rustc --explain E0616`.
最常用的控制可见性的标记就是 pub
关键字,只要外部可以访问这个模块,那么模块中所有标记为 pub
的条目都是对外可见的。这个细节很重要:如果模块(例如:somecrate::somemodule
)本身对外不可见,那么模块中的条目即使标记为 pub
,它们也是对外不可见的。
但是,还有一些 pub
关键字的变体形式,可以用来约束可见性生效的范围:
pub(crate)
:对其所在的 crate 中的其他条目可见。当你需要在 crate 范围提供一个类似助手函数,但是又不希望外部可见是,这个标记就非常实用了。pub(super)
:对当前所在模块的父模块及其子模块可见。这在具有较深层级模块结构的 crate 项目中会很有帮助,它可以让你有选择的扩大可见性。这也是模块级的有效可见性,mod mymodule
本身就对其父模块(或 crate)及其子模块可见。pub(in <path>)
:仅对指定<path>
的代码可见。其中,<path>
必须是对当前模块的祖先模块的描述。这在组织源代码时偶尔会有用,因为它允许将某些功能子集移动到无需在公共 API 中可见的子模块中。例如,Rust 标准库将所有迭代器的适配器合并到一个内部的std::iter::adapters
子模块中,并且具有以下内容:- 子模块中所需的适配器方法都带有
pub(in crate::iter)
可见性标记,例如:std::iter::adapters::map::Map::new
。 - 在外部
std::iter
模块中,对adapters::
中的类型使用pub use
。
- 子模块中所需的适配器方法都带有
pub(self)
:等同于pub(in self)
,也即不是pub
的。比较少见,可以用来减少在代码生成宏时特殊场景的数量。
如果你在一个模块定义了一个私有函数,但是并未在任何地方使用它,Rust 编译器会给出警告:
#![allow(unused)] fn main() { pub mod anothermodule { // 私有函数未被使用 fn inaccessible_fn(x: i32) -> i32 { x + 3 } } }
虽然说警告消息的字面意思说:在本模块“从未使用”,实际上的含义是指由于可见性的限制,你在模块外也无法使用此函数:
#![allow(unused)] fn main() { warning: function `inaccessible_fn` is never used --> src/main.rs:56:8 | 56 | fn inaccessible_fn(x: i32) -> i32 { | ^^^^^^^^^^^^^^^ | = note: `#[warn(dead_code)]` on by default }
可见性的语义
除了前面提到的如何扩大可见范围之外,还有一个问题:何时应该扩大可见范围?普遍可接受的答案是:应当尽可能不扩大可见范围,特别是对于将来会被使用或者重用的代码。
之所以给出这样的建议,第一个原因是已经扩大的可见范围是很难再收缩回来的。一旦 crate 中的某个条目可以被公开访问,就无法将其再改回私有,否则会破坏使用该 crate 的代码,从而不得不发布一个主版本号升级(见第 21 条)。反之则不成立:将一个私有项改为公有通常只需要小版本号的升级,并且不会影响使用 crate 的用户——查阅 Rust API 兼容性指南时你会注意到,其中有很多都和公共访问的条目有关系。
还有一个更重要但是不这么明显的原因就是:选择一个尽可能小的可见性范围,可以让你在将来保留更多的选择权。对公共访问暴露的内容越多,将来需要保持不变的东西也越多(除非进行不兼容的更改)。比如说,你对外暴露了数据结构的内部实现细节,那么将来为了使用更高效的算法而进行变更,就会破坏兼容性;如果你暴露了内部辅助函数,那么不可避免地会有一些外部代码依赖于这些函数的具体细节。
当然,这主要是针对可能有多个用户、较长生命周期的库代码的考虑,但是在平时的项目中也养成这种习惯终究是有益无害的。
值得注意的是,限制可见性的建议并不局限于本篇所述,也不局限于 Rust 这一门语言:
- Rust API 指南中,包含如下建议:
- Effective Java 第 3 版(Addison-Wesley Professional 出版)中有以下建议:
- 第 15 条:最小化类及其成员的可见范围
- 第 16 条:在公共类中,不要使用公共字段,应使用访问方法
- Scott Meyers 在 Effective C++ 第 2 版(Addison-Wesley Professional 出版)中建议:
- 第 18 条:努力让类接口既完整又精简(斜体字为原文所加)。
- 第 20 条:避免在公共接口中使用数据成员。
原文点这里查看