跳到主要内容

模块系统

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 如下:

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.rs

  • rand crate 也有一个入口。因为他是一个库,默认情况下其主入口为 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.rssrc/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.rssrc/math.rs具有完全相同的内容, 并且我们的 src/main.rs 完全不变。

事实上,如果如果我们定义了 math 模块的子模块, folder/mod.rs 结构更加易于理解。

假设我们想添加一个 sub 函数,因为我们强制执行“一个函数一个文件”的限制,我们希望 addsub 存在于各自的模块中。

我们现在的文件结构如下:

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 仍在相同位置。我们只是让它使用 addsub

// 保证 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 也知道 addsub 模块。

所以我们需要在 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 给出了明确的信息 - 现在我们公开了 addsub 模块,我们的 crate 模块结构如下:

crate (我们在这)
`math` 模块
`add` 模块
`add` 函数
`sub` 模块
`sub` 函数

但这和期望略有差距。math 的两个子模块组成涉及实现细节。我们并不希望导出这两个模块 - 我们也不希望任何人直接导入这两个模块!

所以回到声明和导入子模块的地方,让这两个模块变为私有,然后分别重新导出它们的 addsub 函数。

// 子模块是私有的
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 模块的实现细节 - 只有 addsub 函数被导出。

果然,现在 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.rslib.rs 的目录

  • super::foo../foo

  • self::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::*};

同级模块

好吧,同级模块(如 addsub)之间没有直接访问的路径。

如果想在 add 中重新定义 sub,我们在 src/math/sub.rs 不能这样做:

// 编译不通过
pub fn sub(x: i32, y: i32) -> i32 {
add::add(x, -y)
}

addsub 共享父级模块,但不意味他们共享命名空间。

我们也绝对不应该使用第二个 modadd 模块已存在于模块层次结构中的某个位置。 除此之外 - 因为它是 sub 的子模块,它要么存在于 src/math/sub/add.rssrc/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
}

Rust模块组织结构

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()`
Loading Comments...