在了解 Cargo
之前,需要先熟悉Rust
是如何组织代码的。
每个Rust
程序都以root
模块开头。如果你创建的是一个程序库文件,那么root
的模块是lib.rs
。如果你创建的是可执行文件,那么root
的模块通常是main.rs
。当你的代码越来越多时,Rust
允许你将其拆分成模块。为了在组织项目时提供灵活性,有多种方法可以创建模块。
嵌套模块
创建模块最简单的方法是在现有模块中使用mod
代码块,如下:
mod food{
struct Cake;
struct Smoothie;
struct Pizza;
}
fn main(){
let eatable = Cake;
}
上述代码创建了一个名为`food`的内部模块。要在现有模块中创建模块,我们需要使用关键字`mod`,后跟模块名称`food`,之后是一对花括号。在花括号内部,我们可以声明任何类型的元素,甚至嵌套模块。
在我们的food
模块中,我们声明了三种结构:Cake
、Smoothie
和Pizza
。在main
函数中,我们使用路径语法food::Cake
从food
模块创建一个Cake
实例。接下来我们对该程序进行编译:
奇怪的是,编译器提示并未发现任何Cake
类型定义。让我们根据编译器的提示将use food::Cake
添加到代码中:
mod food{
struct Cake;
struct Smoothie;
struct Pizza;
}
use food::Cake;
fn main(){
let eatable = Cake;
}
我们已经将`use food::Cake`添加到代码中。要使用模块中的任何元素,必须添加一个`use`声明。让我们再试一次:
我们得到另一个错误,提示说Cake
是私有的。这为我们提供了关于模块的另一个重要特性,即私密性。默认情况下,模块内的元素是私有的。要使用模块中的任何元素,我们需要将元素纳入作用域。
这需要2个步骤:
- 首先:我们需要通过使用关键字
pub
作为元素的前缀使元素变为公有的。 - 其次:要使用该元素,我们需要添加一个
use
语句,就像之前使用food::Cake
一样。
关键字use
之后的内容是模块中的元素路径。使用路径语法指定模块中任何元素的路径,其语法是在元素名称之间使用双冒号(::
)。路径语法通常以导入元素的模块名称开头,但它也可用于导入某些类型的单个字段,例如枚举。
让我们将Cake
设定为公有的:
mod food{
pub struct Cake;
struct Smoothie;
struct Pizza;
}
use food::Cake;
fn main(){
let eatable = Cake;
}
在`Cake`结构体之前添加了关键字`pub`,并通过`use food::Cake`在`root`模块中引用它。通过这些修改,我们的代码就能够成功编译。
将文件用作模块
模块也可以创建成文件,例如,对文件名为foo
的目录下的文件—main.rs
,我们可以在foo/bar.rs
文件中创建一个名为bar
的模块。然后在main.rs
中,我们需要向编译器告知该模块,即使用mod foo
声明模块。使用基于文件的模块时,这是额外的步骤。
为了掩饰将文件用作模块,我们创建了一个名为modules_demo
的目录,它具有以下结构:
:::info
- modules_demo
└── foo.rs
└── main.rs
:::
foo.rs
中包含一个结构体Bar
,以及它的impl
代码块:
pub struct Bar;
impl Bar {
pub fn init() {
println!("Bar type initialized");
}
}
在main.rs
中使用这个模块,需在main.rs
中添加如下代码:
mod foo;
use crate::foo::Bar;
fn main(){
let _bar = Bar::init();
}
上述代码使用`mod foo`声明了模块foo,然后使用`use crate::foo:Bar`从模块调用结构体`Bar`。注意`use crate::foo::Bar`中的前缀`crate`,这里根据你使用的前缀定义,对应有3种方法可以导入模块中的元素。
绝对导入
crate
:绝对导入前缀,指向当前项目的根目录。在上述代码中是root
模块,即main.rs
文件。任何在关键字crate
之后的内容都会解析成来自root
模块。
相对导入
self
:相对导入前缀,指向与当前模块相关的元素。该前缀用于任何代码想要引用自身包含的模块时,例如use self::foo::Bar
。这主要用于在父模块中重现导出子模块中的元素。super
:相对导入前缀,可以用于从父模块导入元素。诸如tests
这类子模块将使用它从父模块导入元素。例如,如果模块bar
希望访问父模块foo
中的元素Foo
,那么可以使用super::foo::Foo
。将其导入模块bar
将目录用作模块
我们还可以创建一个目录来表示模块。这种方法允许我们将模块中的子模块作为文件和目录的层次结构。假设我们有一个目录my_program
,它有一个名为foo
的模块,并且对应的文件名为foo.rs
。它包含一个名为Bar
的类型和foo
的函数。随着时间的推移,Bar
的API
数量不断增加,我们希望将它们作为子模块进行分离。可以使用基于目录的模块对此用例进行建模。
为了演示将目录用作模块,我们在名为my_program
的目录中创建了一个程序。它在main.rs
中有一个入口点,以及一个名为foo
的目录。该目录现在包含一个名为bar.rs
的子模块。
以下是my_program
目录的结构:
:::info
- my_program
└── foo/
└── bar.rs
└── foo.rs
└── main.rs
:::
为了让`Rust`识别`bar`,我们需要在目录`foo/`旁边创建一个名为`foo.rs`的兄弟文件。`foo.rs`文件将包含目录`foo/`中创建的任何子模块的模块声明(此处为 `bar.rs`)
我们的bar.rs
中包含以下内容:
pub struct Bar;
impl Bar {
pub fn hello() {
println!("Hello from Bar !");
}
}
我们有一个单位结构体bar
,以及关联方法hello()
,同时希望在main.rs
中使用该API
。
:::info 注意:在较旧的 Rust 2015 中,子模块不要求 foo 文件夹和兄弟文件 foo.rs 一同出现, 而是使用 foo 中的 mod.rs 文件向编译器传达该目录是模块。Rust 2018 支持这两种方法。
:::
接下来,我们向foo.rs
中添加一下代码:
mod bar;
pub use self::bar::Bar;
pub fn do_foo() {
println!("Hi from foo!");
}
我们添加了模块`bar`的声明,然后从模块`bar`中重新导出`Bar`的元素。这要求将`Bar`定义为公有的(`pub`)。使用关键字`pub`的部分是从子模块重新导出的元素得以在父模块调用的原因。这里,我们使用关键字`self`来引用当前模块自身。在编写`use`语句时,重新导出一个简便的步骤,这有助于在导入隐藏在嵌套子模块中的元素时消除混乱。
self
是相对导入的关键字。虽然鼓励使用crate
进行绝对导入,但是在从父模块中的子模块重新导出元素时,使用self
的表述更清晰。
最后,main.rs
将能够使用这两个模块:
mod foo;
use foo::Bar;
fn main(){
foo::do_foo();
Bar::hello();
}
我们的`main.rs`声明了`foo`,然后导入结构体`Bar`,接下来我们就可以从`foo`调用`do_foo`方法,并在`Bar`上调用`hello`。
和模块有关的内容不止目前介绍的这些。