模块系统
rust 提供了一个强大的模块系统,可用于按逻辑单元(模块)分层分割代码,并管理它们之间的可见性(公共/私有)。
模块是项目的集合:函数、结构、特征、 impl 块,甚至其他模块。
包 (Crate):一个由多个模块组成的树形结构,可以作为三方库进行分发,也可以生成可执行文件进行运行,它编译后会生成一个可执行文件或者一个库。
cargo new my-project // 创建一个二进制 Package,src/main.rs 是二进制包的根文件
cargo new my-lib --lib // 不能独立运行,只能作为三方库被其它项目引用,根文件是 src/lib.rs
包名就是创新项目的目录名称,在Cargo.toml也会有
[package]
name = "minigrep"
version = "0.1.0"
edition = "2021"
模块 (Module):可以一个文件多个模块,也可以一个文件一个模块,模块可以被认为是真实项目中的代码组织单元
模块路径
绝对路径,以包名或者 crate 作为开头
use minigrep::Config; // minigrep就是package名称
模块可见性
父模块完全无法访问子模块中的私有项,但是子模块却可以访问父模块、父父模块的私有项。
crate
一个 crate 通常来说是一个项目。它有一个 Cargo.toml 文件,这个文件用于声明依赖,入口,构建选项等项目元数据。 每个 crate 可以独立地在crates.io上发表。
假设我们要创建一个二进制(可执行)项目:
cargo new --bin(或者在已有项目上用cargo init --bin).会为新 crate 生成一个Cargo.toml文件。项目入口为
src/main.rs
对于binary crate,src/main.rs 是项目主模块常用路径。不一定是精确路径,可以在 Cargo.toml 添加相应配置,使编译器在别处查看(甚至可以有多个目标二进制文件和多个目标库)。
默认情况下,我们的可执行项目的 src/main.rs 如下:
fn main() {
println!("Hello world!");
}
我们可以通过 cargo run 构建和运行这个项目,若只想构建项目,则运行 cargo build
构建一个 crate 的时候,cargo 下载并编译所有所需依赖,默认情况下把临时文件和最终生成文件放入 ./target/ 目录下。 cargo 既是包管理器又是构建系统。
默认情况下,模块中的项目具有私有可见性,但这可以使用 pub 修饰符覆盖。只能从模块范围之外访问模块的公共项。
// A module named `my_mod`
mod my_mod {
// Items in modules default to private visibility.
fn private_function() {
println!("called `my_mod::private_function()`");
}
// Use the `pub` modifier to override default visibility.
pub fn function() {
println!("called `my_mod::function()`");
}
// Items can access other items in the same module,
// even when private.
pub fn indirect_access() {
print!("called `my_mod::indirect_access()`, that\n> ");
private_function();
}
// Modules can also be nested
pub mod nested {
pub fn function() {
println!("called `my_mod::nested::function()`");
}
#[allow(dead_code)]
fn private_function() {
println!("called `my_mod::nested::private_function()`");
}
// Functions declared using `pub(in path)` syntax are only visible
// within the given path. `path` must be a parent or ancestor module
pub(in crate::my_mod) fn public_function_in_my_mod() {
print!("called `my_mod::nested::public_function_in_my_mod()`, that\n> ");
public_function_in_nested();
}
// Functions declared using `pub(self)` syntax are only visible within
// the current module, which is the same as leaving them private
pub(self) fn public_function_in_nested() {
println!("called `my_mod::nested::public_function_in_nested()`");
}
// Functions declared using `pub(super)` syntax are only visible within
// the parent module
pub(super) fn public_function_in_super_mod() {
println!("called `my_mod::nested::public_function_in_super_mod()`");
}
}
pub fn call_public_function_in_my_mod() {
print!("called `my_mod::call_public_function_in_my_mod()`, that\n> ");
nested::public_function_in_my_mod();
print!("> ");
nested::public_function_in_super_mod();
}
// pub(crate) makes functions visible only within the current crate
pub(crate) fn public_function_in_crate() {
println!("called `my_mod::public_function_in_crate()`");
}
// Nested modules follow the same rules for visibility
mod private_nested {
#[allow(dead_code)]
pub fn function() {
println!("called `my_mod::private_nested::function()`");
}
// Private parent items will still restrict the visibility of a child item,
// even if it is declared as visible within a bigger scope.
#[allow(dead_code)]
pub(crate) fn restricted_function() {
println!("called `my_mod::private_nested::restricted_function()`");
}
}
}
fn function() {
println!("called `function()`");
}
fn main() {
// Modules allow disambiguation between items that have the same name.
function();
my_mod::function();
// Public items, including those inside nested modules, can be
// accessed from outside the parent module.
my_mod::indirect_access();
my_mod::nested::function();
my_mod::call_public_function_in_my_mod();
// pub(crate) items can be called from anywhere in the same crate
my_mod::public_function_in_crate();
// pub(in path) items can only be called from within the module specified
// Error! function `public_function_in_my_mod` is private
//my_mod::nested::public_function_in_my_mod();
// TODO ^ Try uncommenting this line
// Private items of a module cannot be directly accessed, even if
// nested in a public module:
// Error! `private_function` is private
//my_mod::private_function();
// TODO ^ Try uncommenting this line
// Error! `private_function` is private
//my_mod::nested::private_function();
// TODO ^ Try uncommenting this line
// Error! `private_nested` is a private module
//my_mod::private_nested::function();
// TODO ^ Try uncommenting this line
// Error! `private_nested` is a private module
//my_mod::private_nested::restricted_function();
// TODO ^ Try uncommenting this line
}
结构可见性
结构体的字段具有额外的可见性。可见性默认为私有,可以使用 pub 修饰符覆盖。仅当从定义它的模块外部访问结构体时,这种可见性才重要,并且其目标是隐藏信息(封装)。
mod my {
// A public struct with a public field of generic type `T`
pub struct OpenBox<T> {
pub contents: T,
}
// A public struct with a private field of generic type `T`
pub struct ClosedBox<T> {
contents: T,
}
impl<T> ClosedBox<T> {
// A public constructor method
pub fn new(contents: T) -> ClosedBox<T> {
ClosedBox {
contents: contents,
}
}
}
}
fn main() {
// Public structs with public fields can be constructed as usual
let open_box = my::OpenBox { contents: "public information" };
// and their fields can be normally accessed.
println!("The open box contains: {}", open_box.contents);
// Public structs with private fields cannot be constructed using field names.
// Error! `ClosedBox` has private fields
//let closed_box = my::ClosedBox { contents: "classified information" };
// TODO ^ Try uncommenting this line
// However, structs with private fields can be created using
// public constructors
let _closed_box = my::ClosedBox::new("classified information");
// and the private fields of a public struct cannot be accessed.
// Error! The `contents` field is private
//println!("The closed box contains: {}", _closed_box.contents);
// TODO ^ Try uncommenting this line
}
dependencies
让我们向刚才创建的 crate 添加 rand 依赖来看看命名空间是怎么工作的。我们需要修改 Cargo.toml,其内容如下:
[package]
name = "modules"
version = "0.1.0"
edition = "2018"
[dependencies]
rand = "0.7.0"
现在让我们在 src/main.rs 里使用 rand, src/main.rs 如下:
fn main() {
let random_boolean = rand::random();
println!("You {}!", if random_boolean { "win" } else { "lose" });
}
请注意:
我们不需要使用
use指令来使用rand- 它在项目下的文件全局可用,因为它在Cargo.toml中被声明为依赖(rust 2018之前的版本则不是这样)我们完全没必要使用
mod(稍后讲述)
为了明白这篇博客的余下部分,你需要明白 rust 模块仅仅是命名空间 - 他们让你把相关符号组合在一起并保证可见性规则。
我们的 crate 有一个主模块(我们现在所在),它的源在
src/main.rsrandcrate 也有一个入口。因为他是一个库,默认情况下其主入口为src/lib.rs在我们主模块范围,我们可以在主模块通过依赖名称使用依赖
总之,我们现在只处理两个模块:我们项目主入口还有 rand 的入口。
use
如果我们不喜欢一直这样写 rand::random(),我们可以把 random 注入主模块范围。
use rand::random;
// 我们可以通过 `rand::random()` 或 `random()` 来使用它
fn main() {
if random() && random() {
println!("You won twice in a row!");
} else {
println!("Try again...");
}
}
我们也可以使用通配符来导入 rand 主模块导出的所有符号。
// 这会导入 random,还有 thead_rng 等
use rand::*;
fn main() {
if random() {
panic!("Unlucky coin toss");
}
println!("Hello world");
}
mod
in the same file
正如刚才所见,模块是一个让你组合相关符号的语言结构。
让我们修改下 src/main.rs 来证明这个观点:
mod math {
pub fn add(x: i32, y: i32) -> i32 {
x + y
}
// 使用 `pub` 来导出 `add()` 函数
// 如果不这样做,`add()` 会变为 `math` 模块的私有函数
// 我们将无法在 `math` 模块外使用它
}
fn main() {
let result = math::add(1, 2);
println!("1 + 2 = {}", result);
}
从范围角度,我们项目结构如下:
我们 crate 的主模块
`math`: 我们的 `math` 模块
`rand`: `rand` crate 的主模块
从文件角度,主模块和 math 模块都在同一个文件 src/main.rs 下。
in the separated file
现在,如果我们如下修改项目:
pub fn add(x: i32, y: i32) -> i32 {
x + y
}
fn main() {
let result = math::add(1, 2);
println!("1 + 2 = {}", result);
}
然而这行不通。
Compiling modules v0.1.0 (/home/amos/Dev/modules)
error[E0433]: failed to resolve: use of undeclared type or module `math`
--> src/main.rs:2:18
|
2 | let result = math::add(1, 2);
| ^^^^ use of undeclared type or module `math`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0433`.
error: Could not compile `modules`.
To learn more, run the command again with --verbose.
虽然 src/main.rs 和 src/lib.rs(二进制和库项目)会被 cargo 自动识别为程序入口,其他文件则需要在文件中明确声明。
我们的错误在于仅仅创建了 src/math.rs 文件,希望 cargo 会在构建时找到它,但事实上并不是这样的。cargo 甚至不会解析它。 cargo check 命令也不会报错,因为 src/math.rs 现在还不是 crate 源文件的一部分。
方法
mod math {
include!("math.rs");
}
// 注意: 这不是符合 rust 风格的写法,仅作 mod 学习用
fn main() {
let result = math::add(1, 2);
println!("1 + 2 = {}", result);
}
现在 crate 可以编译和运行了,因为:
我们定义了一个名为
math的模块我们告诉编译器复制/粘贴其他文件(
math.rs)到模块代码块中参考 include! 文档
但这不是通常导入模块的方式。按照惯例,如果使用不跟随代码块的 mod 指令,效果上述一样。
所以也可以这样写:
mod math;
fn main() {
let result = math::add(1, 2);
println!("1 + 2 = {}", result);
}
就是这么简单。但容易混淆之处在于,根据 mod 之后是否有代码块,它可以内联定义模块,或者导入其他文件。
这也解释了为什么在 src/math.rs 里不用再定义另一个 mod math {}。因为 src/math.rs 已经在 src/main.rs 中导入,它已经说 src/math.rs 的代码存在于一个名为 math 的模块中。
use
use 声明可用于将完整路径绑定到新名称,以便于访问。它经常这样使用:
use crate::deeply::nested::{
my_first_function,
my_second_function,
AndATraitType
};
fn main() {
my_first_function();
}
现在我们几乎了解了 mod,那 use 呢?
use 的唯一目的是将符号带入命名空间,让符号使用更加简短。
特别是,use 永远不会告诉编译器去编译 mod 导入文件之外的其他文件。
在 main.rs/math.rs 例子中,在 src/main.rs 写下如下语句时:
mod math;
我们在主模块导入一个名为 math 模块,这个模块导出 add 函数。
从范围角度,结构如下:
crate 主模块(我们在这儿)
`math` 模块
`add` 函数
这就是为什么我们要使用 add 函数时要这样引用 math::add,即从主模块到 add 函数的正确路径。
请注意,如果我们从另一个模块调用 add,那么 math::add 可能不是有效路径。 然而,add 有一个更长的添加路径,即 crate::math::add - 它在我们的 crate 中的任何位置都有效(只要 math 模块保持原样)。
所以,如果我们不想每次都使用 math:: 前缀调用 add,可以用 use 指令:
mod math;
use math::add;
fn main() {
// 看,没有前缀了!
let result = add(1, 2);
println!("1 + 2 = {}", result);
}
您可以使用 as 关键字将导入绑定到不同的名称:
// Bind the `deeply::nested::function` path to `other_function`.
use deeply::nested::function as other_function;
fn function() {
println!("called `function()`");
}
mod deeply {
pub mod nested {
pub fn function() {
println!("called `deeply::nested::function()`");
}
}
}
fn main() {
// Easier access to `deeply::nested::function`
other_function();
println!("Entering block");
{
// This is equivalent to `use deeply::nested::function as function`.
// This `function()` will shadow the outer one.
use crate::deeply::nested::function;
// `use` bindings have a local scope. In this case, the
// shadowing of `function()` is only in this block.
function();
println!("Leaving block");
}
function();
}
那 mod.rs 又是什么呢?
好吧,我说谎了 - 我们还没完全了解 mod。
目前,crate 有一个漂亮又扁平的文件结构:
src/
main.rs
math.rs
这是有道理的,因为 math 是一个小模块(只有一个函数),它并不需要拥有自己的文件夹。但我们也可以这样改变它的结构:
src/
main.rs
math/
mod.rs
(对于那些熟悉 node.js 的人来说,mod.rs 类似于 index.js)。
就命名空间/范围而言,两种结构都是等价的。我们的新 src/math/mod.rs 与src/math.rs具有完全相同的内容, 并且我们的 src/main.rs 完全不变。
事实上,如果如果我们定义了 math 模块的子模块, folder/mod.rs 结构更加易于理解。
假设我们想添加一个 sub 函数,因为我们强制执行“一个函数一个文件”的限制,我们希望 add 和 sub 存在于各自的模块中。
我们现在的文件结构如下:
src/
main.rs
math/
mod.rs
add.rs (新文件!)
sub.rs (也是新文件!)
概念上而言,命名空间树如下:
crate (src/main.rs)
`math` 模块 (src/math/mod.rs)
`add` 模块 (src/math/add.rs)
`sub` 模块 (src/math/sub.rs)
我们的 src/main.rs 不需要做很大改动 - math 仍在相同位置。我们只是让它使用 add 和 sub:
// 保证 math 在 `./math.rs` 或 `./math/mod.rs` 中定义
mod math;
// 将两个符号带入范围,在 `math` 模块中保证都已导出
use math::{add, sub};
fn main() {
let result = add(1, 2);
println!("1 + 2 = {}", result);
}
我们的 src/math/add.rs 正如我们在 math 模块做的一样:定义一个函数,并用 pub 将其导出。
pub fn add(x: i32, y: i32) -> i32 {
x + y
}
类似地,src/math/sub.rs 文件如下:
pub fn sub(x: i32, y: i32) -> i32 {
x - y
}
现在来看 src/math/mod.rs。我们知道 cargo 知道 math 这个模块存在, 因为 src/main.rs 中的 mod math; 语句已将其导入。 但我们需要让 cargo 也知道 add 和 sub 模块。
所以我们需要在 src/math/mod.rs 添加如下语句;
mod add;
mod sub;
现在 cargo 知晓所有源文件。
crate 能编译成功吗?(剧透一下:没有哦)
Compiling modules v0.1.0 (/home/amos/Dev/modules)
error[E0603]: module `add` is private
--> src/main.rs:2:12
|
2 | use math::{add, sub};
| ^^^
error[E0603]: module `sub` is private
--> src/main.rs:2:17
|
2 | use math::{add, sub};
| ^^^
发生了什么?好吧,按现在的写法,主模块看起来是这样的:
crate (我们在这儿)
`math` 模块
(空的)
所以 math::add 不是一个有效路径,因为 math 模块没有导出任何东西。
好吧,我猜我们可以直接在 mod 前加上 pub?
将 src/math/mod.rs 做如下修改:
pub mod add;
pub mod sub;
又一次,编译不通过:
Compiling modules v0.1.0 (/home/amos/Dev/modules)
error[E0423]: expected function, found module `add`
--> src/main.rs:5:18
|
5 | let result = add(1, 2);
| ^^^ not a function
help: possible better candidate is found in another module, you can import it into scope
|
2 | use crate::math::add::add;
|
rustc 给出了明确的信息 - 现在我们公开了 add 和 sub 模块,我们的 crate 模块结构如下:
crate (我们在这)
`math` 模块
`add` 模块
`add` 函数
`sub` 模块
`sub` 函数
但这和期望略有差距。math 的两个子模块组成涉及实现细节。我们并不希望导出这两个模块 - 我们也不希望任何人直接导入这两个模块!
所以回到声明和导入子模块的地方,让这两个模块变为私有,然后分别重新导出它们的 add 和 sub 函数。
// 子模块是私有的
mod add;
mod sub;
// 这些是重导出函数
pub use add::add;
pub use sub::sub;
这样改变后,从 src/math/mod.rs 角度看,模块结构如下:
`math` 模块(我们在这)
`add` 函数(公开)
`sub` 函数(公开)
`add` 模块(私有)
`add` 函数(公开)
`sub` 模块(私有)
`sub` 函数(公开)
然而,从 src/main.rs 角度看,模块结构如下:
crate (你在这)
`math` 模块
`add` 模块
`sub` 模块
我们已经成功隐藏 math 模块的实现细节 - 只有 add 和 sub 函数被导出。
果然,现在 crate 编译成功且运行良好。
回顾
src/main.rs
mod math;
use math::{add, sub};
fn main() {
let result = add(1, 2);
println!("1 + 2 = {}", result);
}
src/math/mod.rs
mod add;
mod sub;
pub use add::add;
pub use sub::sub;
src/math/add.rs
pub fn add(x: i32, y: i32) -> i32 {
x + y
}
src/math/sub.rs
pub fn sub(x: i32, y: i32) -> i32 {
x - y
}
父模块
目前我们仅使用了那些命名空间/符号树深处的符号。
但如果需要,我们也可以使用父级命名空间里。
假设我们希望 math 模块有一个模块级的常量来开启或关闭日志。
(注意,这样控制日志是一个糟糕的做法,我只是暂时想不到其他愚蠢的例子)。
现在将 src/math/mod.rs 做如下修改:
mod add;
mod sub;
pub use add::add;
pub use sub::sub;
const DEBUG: bool = true;
然后我们可以在其他模块引用 DEBUG,比如 src/math/add.rs:
pub fn add(x: i32, y: i32) -> i32 {
if super::DEBUG {
println!("add({}, {})", x, y);
}
x + y
}
意料之中,编译通过且成功运行:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/modules`
add(1, 2)
1 + 2 = 3
注意:一个模块总是可以访问其父级作用域(通过 super::)- 即便是是父级作用域的私有变量、私有函数等。 DEBUG 是私有的,但我们可以在 add 模块中使用它。
如果我们要定义rust关键字和文件路径惯用语之间的对应关系,我们可以映射:
crate::foo对/foo- 如果我们认为“根文件系统”为包含main.rs或lib.rs的目录super::foo对../fooself::foo对./foo
什么时候会需要使用 self 呢?
好吧,对于 src/math/mod.rs 如下两行:
pub use add::add;
pub use sub::sub;
我们可以用单行代码实现:
pub use self::{add:add, sub::sub};
假设子模块只导出了我们希望使用的符号,我们甚至可以使用通配符:
pub use self::{add::*, sub::*};
同级模块
好吧,同级模块(如 add 和 sub)之间没有直接访问的路径。
如果想在 add 中重新定义 sub,我们在 src/math/sub.rs 不能这样做:
// 编译不通过
pub fn sub(x: i32, y: i32) -> i32 {
add::add(x, -y)
}
add 和 sub 共享父级模块,但不意味他们共享命名空间。
我们也绝对不应该使用第二个 mod。 add 模块已存在于模块层次结构中的某个位置。 除此之外 - 因为它是 sub 的子模块,它要么存在于 src/math/sub/add.rs 或 src/math/sub/add/mod.rs 中 - 这两者都没有意义。
如果我们想访问 add, 必须通过父级模块,就像其他人一样。在 src/math/sub.rs 中:
pub fn sub(x: i32, y: i32) -> i32 {
super::add::add(x, -y)
}
或者使用 src/math/mod.rs 重新导出的 add:
pub fn sub(x: i32, y: i32) -> i32 {
super::add(x, -y)
}
或者简单地导入 add 模块下的所有东西:
pub fn sub(x: i32, y: i32) -> i32 {
use super::add::*;
add(x, -y)
}
请注意,函数有它自己的作用域,所以 use 不会影响这个模块其他地方。
你甚至可以用 {} 限制作用域!
pub fn sub(x: i32, y: i32) -> i32 {
let add = "something else";
let res = {
// 在这个代码块中,`add` 是 `add` 模块导出的函数
use super::add::*;
add(x, -y)
};
// 现在我们离开代码块,`add` 又变为 "something else"
res
}
super self
可以在路径中使用 super 和 self 关键字,以消除访问项目时的歧义并防止对路径进行不必要的硬编码。
fn function() {
println!("called `function()`");
}
mod cool {
pub fn function() {
println!("called `cool::function()`");
}
}
mod my {
fn function() {
println!("called `my::function()`");
}
mod cool {
pub fn function() {
println!("called `my::cool::function()`");
}
}
pub fn indirect_call() {
// Let's access all the functions named `function` from this scope!
print!("called `my::indirect_call()`, that\n> ");
// The `self` keyword refers to the current module scope - in this case `my`.
// Calling `self::function()` and calling `function()` directly both give
// the same result, because they refer to the same function.
self::function();
function();
// We can also use `self` to access another module inside `my`:
self::cool::function();
// The `super` keyword refers to the parent scope (outside the `my` module).
super::function();
// This will bind to the `cool::function` in the *crate* scope.
// In this case the crate scope is the outermost scope.
{
use crate::cool::function as root_function;
root_function();
}
}
}
fn main() {
my::indirect_call();
}
文件层次结构
模块可以映射到文件/目录层次结构。让我们分解一下文件中的可见性示例:
$ tree .
.
├── my
│ ├── inaccessible.rs
│ └── nested.rs
├── my.rs
└── split.rs
在 split.rs 中:
// This declaration will look for a file named `my.rs` and will
// insert its contents inside a module named `my` under this scope
mod my;
fn function() {
println!("called `function()`");
}
fn main() {
my::function();
function();
my::indirect_access();
my::nested::function();
}
在 my.rs 中:
// Similarly `mod inaccessible` and `mod nested` will locate the `nested.rs`
// and `inaccessible.rs` files and insert them here under their respective
// modules
mod inaccessible;
pub mod nested;
pub fn function() {
println!("called `my::function()`");
}
fn private_function() {
println!("called `my::private_function()`");
}
pub fn indirect_access() {
print!("called `my::indirect_access()`, that\n> ");
private_function();
}
在 my/nested.rs 中:
pub fn function() {
println!("called `my::nested::function()`");
}
#[allow(dead_code)]
fn private_function() {
println!("called `my::nested::private_function()`");
}
在 my/inaccessible.rs 中:
#[allow(dead_code)]
pub fn public_function() {
println!("called `my::inaccessible::public_function()`");
}
让我们检查一下事情是否仍然像以前一样工作:
$ rustc split.rs && ./split
called `my::function()`
called `function()`
called `my::indirect_access()`, that
> called `my::private_function()`
called `my::nested::function()`