Rust-使用模块组织和复用代码

一、包与安装器

$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

二、定义模块以控制范围和隐私

我们将讨论模块和模块系统的其他部分,即允许您命名项目的路径;use将路径带入作用域的关键字;和pub使项目公开的关键字。我们还将讨论as关键字、外部包和 glob 运算符。现在,让我们专注于模块!

模块让我们可以将包中的代码组织成组,以提高可读性和易于重用。模块还控制项目的隐私,即项目是否可以被外部代码使用 ( public ) 或者是内部实现细节而不可供外部使用 ( private )。

创建一个名为restaurant运行的新库 cargo new --lib restaurant:

mod front_of_house {
mod hosting {
fn add_to_waitlist() {}

fn seat_at_table() {}
}

mod serving {
fn take_order() {}

fn serve_order() {}

fn take_payment() {}
}
}

我们通过以mod关键字开头,然后指定模块的名称(在本例中为front_of_house)并在模块主体周围放置大括号来定义模块。
在模块内部,我们可以有其他模块,如本例中的模块hosting和serving。模块还可以保存其他项目的定义,例如结构体、枚举、常量、特征。

通过使用模块,我们可以将相关定义组合在一起并命名它们相关的原因。使用此代码的程序员可以更轻松地找到他们想要使用的定义,因为他们可以根据组浏览代码,而不必通读所有定义。
我们来看看这个配置的模块数。

crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment

这棵树显示了一些模块如何相互嵌套。请注意,整个模块树都以名为的隐式模块为根 crate.

三、引用模块树中项目的路径

为了显示 Rust 在哪里可以找到模块树中的项目,我们使用路径的方式与导航文件系统时使用路径的方式相同。如果我们想调用一个函数,我们需要知道它的路径。

路径可以采用两种形式:

  • 绝对路径通过使用crate名称或字面crate根开始。
  • 相对路径从当前模块开始,并在当前模块中使用self、super或标识符。
    绝对路径和相对路径都后跟一个或多个由双冒号(::)分隔的标识符。

我们将展示两种add_to_waitlist从eat_at_restaurantcrate 根中定义的新函数调用该函数的方法。该eat_at_restaurant函数是我们库 crate 公共 API 的一部分,因此我们用pub关键字对其进行标记。

mod front_of_house {
mod hosting {
fn add_to_waitlist() {}

fn seat_at_table() {}
}

mod serving {
fn take_order() {}

fn serve_order() {}

fn take_payment() {}
}
}

pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}

选择使用相对路径还是绝对路径是您将根据项目做出的决定。该决定应取决于是否更有可能将项目定义代码与使用该项目的代码分开移动还是与该代码一起移动。
但是上述代码编译不通过:
cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^

error[E0603]: module `hosting` is private
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors

错误消息说该模块hosting是私有的。换句话说,我们有hosting模块和add_to_waitlist 函数的正确路径,但 Rust 不允许我们使用它们,因为它无法访问私有部分。
模块不仅仅用于组织代码。它们还定义了 Rust 的隐私边界:封装实现细节的行不允许外部代码知道、调用或依赖。

Rust 中隐私的工作方式是所有项目(函数、方法、结构、枚举、模块和常量)默认都是私有的。父模块中的项不能使用子模块中的私有项,但子模块中的项可以使用其祖先模块中的项。原因是子模块包装并隐藏了它们的实现细节,但子模块可以看到定义它们的上下文。

Rust 选择以这种方式拥有模块系统功能,因此隐藏内部实现细节是默认的。这样,您就知道可以在不破坏外部代码的情况下更改内部代码的哪些部分。但是您可以通过使用 pub 关键字将项目公开来将子模块代码的内部部分暴露给外部祖先模块。

使用pub关键字公开路径

我们希望eat_at_restaurant父模块中的add_to_waitlist函数可以访问子模块中的函数,因此我们hosting用pub关键字标记该模块。

mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();

// Relative path
front_of_house::hosting::add_to_waitlist();
}

现在代码将编译!让我们看看绝对路径和相对路径,并仔细检查为什么添加pub关键字让我们add_to_waitlist在隐私规则方面使用这些路径。

从相对路径开始 super

我们还可以通过super在路径的开头使用来构造在父模块中开始的相对路径。这就像使用..语法启动文件系统路径。我们为什么要这样做?

该函数通过指定以开头的路径来fix_incorrect_order调用该函数:serve_order serve_order super

fn serve_order() {}

mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}

fn cook_order() {}
}

该fix_incorrect_order函数位于back_of_house模块中,因此我们可以使用super转到的父模块back_of_house。

公开的结构体和枚举

我们还可以使用pub将结构体和枚举指定为公共的,但还有一些额外的细节。如果我们pub在结构定义之前使用,我们将结构公开,但结构的字段仍然是私有的。我们可以根据具体情况公开或不公开每个字段。

mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}

impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}

pub fn eat_at_restaurant() {
// Order a breakfast in the summer with Rye toast
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);

// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal
// meal.seasonal_fruit = String::from("blueberries");
}

因为结构体中的toast字段back_of_house::Breakfast是公共的,所以eat_at_restaurant我们可以toast使用点表示法对该字段进行写入和读取。请注意,我们不能使用seasonal_fruit字段 in eat_at_restaurant因为seasonal_fruit是私有的。尝试取消注释修改seasonal_fruit字段值的行。

另外,请注意,因为back_of_house::Breakfast有一个私有字段,该结构需要提供一个公共关联函数来构造一个实例Breakfast(我们在summer这里命名了它)。如果Breakfast没有这样的函数,我们就无法创建Breakfastin 的实例,eat_at_restaurant因为我们无法设置seasonal_fruitin的私有字段的值 eat_at_restaurant。

相比之下,如果我们公开一个枚举,那么它的所有变体都是公开的。我们只需要pubbeforeenum关键字,

mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}

pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}

四、使用use关键字将路径纳入范围

到目前为止,我们编写的用于调用函数的路径似乎很长且重复。无论我们选择add_to_waitlist函数的绝对路径还是相对路径,每次我们要调用时add_to_waitlist都必须指定front_of_houseand hosting。幸运的是,有一种方法可以简化这个过程。我们可以将路径带入作用域一次,然后调用该路径中的项目,就好像它们是带有use关键字的本地项目一样。

mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}

use在作用域中添加一个路径类似于在文件系统中创建一个符号链接。通过use crate::front_of_house::hosting在 crate 根中添加,hosting现在是该范围内的有效名称,就像hosting 模块已在 crate 根中定义一样。进入范围的路径use 也检查隐私,就像任何其他路径一样。

还可以use使用相对路径将项目带入范围。

mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

use self::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}

创建惯用use路径

在调用函数时指定父模块可以清楚地表明该函数不是本地定义的,同时仍然最大限度地减少完整路径的重复。

mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
add_to_waitlist();
add_to_waitlist();
add_to_waitlist();
}

另一方面,当使用引入结构、枚举和其他项目时use,指定完整路径是惯用的做法。
use std::collections::HashMap;

fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}

如果我们将两个同名的项目带入use语句的作用域,因为 Rust 不允许这样做。

use std::fmt;
use std::io;

fn function1() -> fmt::Result {
// --snip--
}

fn function2() -> io::Result<()> {
// --snip--
}

使用as关键字提供新名称

将两种同名类型带入同一作用域的问题还有另一种解决方案use:在路径之后,我们可以as为该类型指定一个新的本地名称或别名。

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
// --snip--
}

fn function2() -> IoResult<()> {
// --snip--
}

在第二个use语句中,我们IoResult为 std::io::Result类型选择了新名称,这不会与我们也引入范围的Resultfrom冲突std::fmt。

重新导出名称 pub use

当我们使用use关键字将名称带入作用域时,新作用域中可用的名称是私有的。为了使调用我们的代码的代码能够引用该名称,就好像它已在该代码的范围内定义一样,我们可以组合pub 和use。这种技术称为重新导出,因为我们将一个项目带入范围,同时也使其他人可以将该项目带入他们的范围。

mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}

使用外部包

我们编写了一个猜谜游戏项目,该项目使用一个名为rand获取随机数的外部包。为了rand在我们的项目中使用,我们在Cargo.toml 中添加了这一行:

rand = "0.8.3"

然后,为了将rand定义引入我们的包范围,我们添加了use一行以 crate 的名称开头的行rand,并列出了我们想要引入范围的项目。
use rand::Rng;

fn main() {
let secret_number = rand::thread_rng().gen_range(1..101);
}

请注意,标准库 (std) 也是我们包外部的 crate。因为标准库是随 Rust 语言一起提供的,所以我们不需要将Cargo.toml更改为包含std. 但是我们确实需要引用它use以将项目从那里引入我们的包的范围。
use std::collections::HashMap;

这是一个std以标准库包的名称开头的绝对路径。

使用嵌套路径清理大型使用列表

如果我们使用在同一个 crate 或同一个模块中定义的多个项目,在单独的行中列出每个项目可能会占用我们文件中的大量垂直空间。

// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--

相反,我们可以使用嵌套路径将相同的项目在一行中引入范围。为此,我们指定路径的公共部分,后跟两个冒号,然后用大括号将路径中不同部分的列表括起来:
// --snip--
use std::{cmp::Ordering, io};
// --snip--

在更大的程序中,使用嵌套路径将许多项从同一个 crate 或模块引入范围可以减少use很多需要的单独语句的数量!
我们可以在路径中的任何级别使用嵌套路径,这在组合use共享子路径的两个语句时非常有用。
use std::io;
use std::io::Write;

这两条路径的共同部分是std::io,这就是完整的第一条路径。要将这两个路径合并为一个use语句,我们可以self在嵌套路径中使用,
use std::io::{self, Write};

这条线带来std::io并std::io::Write进入范围。

全局运算符

如果我们想将路径中定义的所有公共项引入作用域,我们可以指定该路径,后跟*glob 运算符:

use std::collections::*;

此use语句将中定义的所有公共项std::collections引入当前范围。使用 glob 运算符时要小心!Glob 可以让您更难分辨哪些名称在范围内,以及在您的程序中使用的名称是在哪里定义的。

五、将模块分成不同的文件

到目前为止,本章中的所有示例都在一个文件中定义了多个模块。当模块变大时,您可能希望将它们的定义移动到单独的文件中,以使代码更易于导航。
通过更改 crate 根文件将front_of_house模块移动到它自己的文件src/front_of_house.rs 中。
src/lib.rs:

mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}

src/front_of_house.rs:
pub mod hosting {
pub fn add_to_waitlist() {}
}

在之后使用分号mod front_of_house而不是使用块告诉 Rust 从另一个与模块同名的文件中加载模块的内容。为了继续我们的示例并将hosting模块也提取到它自己的文件中,我们将src/front_of_house.rs更改为仅包含hosting模块的声明:
src/front_of_house.rs:
pub mod hosting;

然后我们创建一个src/front_of_house目录和一个文件 src/front_of_house/hosting.rs来包含hosting模块中的定义:
src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}

模块树保持不变,eat_at_restaurant 即使定义存在于不同的文件中,函数调用也无需任何修改即可工作。这种技术使您可以随着模块大小的增长将模块移动到新文件中。
请注意,src/lib.rs中的pub use crate::front_of_house::hosting语句也没有改变,也不会对编译为 crate 一部分的文件产生任何影响。