上一篇:06-枚举和模式匹配
当你编写大型程序时,组织代码将变得越来越重要。通过对相关功能进行分组并将具有不同功能的代码分开,您可以明确在哪里可以找到实现特定功能的代码,以及在哪里可以改变功能的工作方式。
到目前为止,我们编写的程序都是一个文件中的一个模块。随着项目的发展,你应该把代码分成多个模块,然后再分成多个文件。一个软件包可以包含多个二进制包和一个库包。随着软件包的增长,你可以将部分代码提取到独立的crates中,成为外部依赖。本章将介绍所有这些技术。对于由一系列相互关联、共同发展的软件包组成的大型项目,Cargo 提供了工作空间。
我们还将讨论封装实现细节的问题,这可以让你在更高层次上重用代码:一旦你实现了某个操作,其他代码就可以通过它的公共接口调用你的代码,而不必知道它是如何实现的。你编写代码的方式定义了哪些部分是公共的,供其他代码使用,哪些部分是私有的实现细节,你保留更改的权利。这是另一种限制你必须记在脑子里的细节数量的方法。
一个相关的概念是作用域:编写代码的嵌套上下文有一组被定义为 "作用域 "的名称。在读取、编写和编译代码时,程序员和编译器需要知道特定位置的特定名称是否指代变量、函数、结构体、枚举、模块、常量或其他项目,以及该项目的含义。你可以创建作用域,改变哪些名称在作用域内,哪些在作用域外。在同一个作用域中不能有两个同名的项目;可以使用工具来解决名称冲突。
Rust 有许多功能允许你管理代码的组织,包括哪些细节是公开的,哪些细节是私有的,以及程序中每个作用域中的名称。这些功能有时统称为模块系统,包括:
①. Packages:让您构建、测试和共享crates;
②. Crates:生成库或可执行文件的模块树;
③. Modules和use: 让您控制路径的组织、范围和私密性;
④. Path:命名项目(如结构、函数或模块)的一种方式;
在本章中,我们将介绍所有这些功能,讨论它们如何交互,并解释如何使用它们来管理作用域。到本章结束时,你应该对模块系统有了扎实的了解,并能像专家一样使用作用域!
7.1 Packages和Crates
我们要介绍的模块系统的第一个部分是Packages和Crates。
crate 是 Rust 编译器每次考虑的最小代码量。即使你运行的是 rustc 而不是 cargo ,并传递一个源代码文件,编译器也会将该文件视为一个crate。Crate 可以包含模块,模块可以在其他文件中定义,这些文件将与 Crate 一起编译。
crate有两种形式:二进制、库。二进制crate是可以编译成可执行文件运行的程序,例如命令行程序或服务器。每个二进制板crate都必须有一个名为 main 的函数,用于定义可执行文件运行时会发生什么。到目前为止,我们创建的所有crate都是二进制。
库crate没有 main 功能,也不会编译成可执行文件。相反,它们定义的功能旨在与多个项目共享。例如,我们使用的 rand crate 提供了生成随机数的功能。大多数时候,当 Rustaceans 说 "crate "时,他们指的是库 crate,而且他们将 "crate "与一般编程概念 "library "互换使用。
与C/C++代码中的bin和so/a库一样;
crate根文件是 Rust 编译器的源文件,它构成了crate的根模块。
Packages是由一个或多个 crate 组成的捆绑包,提供一系列功能。软件包包含一个 Cargo.toml 文件,该文件描述了如何构建这些板块。Cargo 实际上是一个软件包,它包含了你用来构建代码的命令行工具的二进制原型。Cargo 软件包还包含二进制原型所依赖的库原型。其他项目可以依赖 Cargo 库原型来使用与 Cargo 命令行工具相同的逻辑。
一个软件包可以包含任意数量的二进制包,但最多只能包含一个库包。一个软件包必须至少包含一个原型包,无论是库原型包还是二进制原型包。
让我们来看看创建软件包时会发生什么。首先,我们输入命令 cargo new :
PS D:\rustProj> cargo.exe new my-poject
Created binary (application) `my-poject` package
PS D:\rustProj> ls .\my-poject\
目录: D:\rustProj\my-poject
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2024/1/24 14:49 src
-a---- 2024/1/24 14:49 8 .gitignore
-a---- 2024/1/24 14:49 178 Cargo.toml
PS D:\rustProj>ls .\my-poject\src\
目录: D:\rustProj\my-poject\src
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2024/1/24 14:49 45 main.rs
运行 cargo new 后,我们使用 ls 查看 Cargo 创建了什么。在项目目录中,有一个 Cargo.toml 文件,为我们提供了一个软件包。还有一个包含 main.rs 的 src 目录。用文本编辑器打开 Cargo.toml,注意其中没有提到 src/main.rs。Cargo 遵循的惯例是,src/main.rs 是与软件包同名的二进制 crate 的 crate 根目录。同样,Cargo 知道,如果软件包目录中包含 src/lib.rs,那么软件包中就包含一个与软件包同名的库 crate,而 src/lib.rs 就是它的 crate 根目录。Cargo 会将 crate 根文件传给 rustc ,以便构建库或二进制文件。
这里,我们有一个只包含 src/main.rs 的软件包,这意味着它只包含一个名为 my-project 的二进制crate。如果软件包包含 src/main.rs 和 src/lib.rs,那么它就有两个crate:一个二进制和一个库,二者都与软件包同名。通过在 src/bin 目录中放置文件,软件包可以拥有多个二进制包:每个文件都是一个单独的二进制包。
7.2 定义Module以控制范围和隐私
在本节中,我们将讨论模块和模块系统的其他部分,即允许命名项目的路径、将路径引入作用域的 use 关键字,以及使项目公开的 pub 关键字。我们还将讨论 as 关键字、外部包和 glob 操作符。
首先,我们将列出一个规则列表,以便将来组织代码时参考。然后,我们将详细解释每一条规则。
7.2.1 模块清单
在此,我们将快速介绍模块、路径、 use 关键字和 pub 关键字在编译器中的工作方式,以及大多数开发人员组织代码的方式。我们将在本章中逐一举例说明这些规则,但这是一个很好的参考,可以提醒我们模块是如何工作的。
①. 从crate根目录启动:从crate根文件开始编译板crates时,编译器首先会在create根文件(通常是src/lib.rs,或src/main.rs)中查找要编译的代码。
②. 声明Modules:在 crate 根文件中,你可以声明新模块;比如,你声明了一个 "garden "模块,文件名是:mod garden; 。编译器会在这些地方查找模块代码:
Ⅰ. 内联,置于大括号内,取代mod garden后面的分号 ;
Ⅱ. 在文件 src/garden.rs 中;
Ⅲ. 在文件 src/garden/mod.rs 中;
③. 声明子Modules:在 crate 根目录以外的任何文件中,都可以声明子模块。例如,你可以在 src/garden.rs 中声明 mod vegetables; 。编译器将在这些地方的父模块目录中查找子模块的代码:
Ⅰ. 内联,直接跟在mod vegetables后面,用大括号代替分号;
Ⅱ. 在文件 src/garden/vegetables.rs 中;
Ⅲ. 在文件 src/garden/vegetables/mod.rs 中;
④. 模块中的代码路径:一旦某个模块成为你的crate的一部分,只要隐私规则允许,你就可以使用代码的路径从同一crate中的任何地方引用该模块中的代码。例如,garden/vegetables模块中的 Asparagus 类型可在 crate::garden::vegetables::Asparagus 找到。
⑤. Public VS Private:默认情况下,模块内的代码对其父模块是私有的。若要将模块变为公共模块,请使用 pub mod 而不是 mod 进行声明。若要将公共模块中的项目也设为公共模块,请在其声明前使用 pub 。
⑥. use关键字:在作用域中, use 关键字为项目创建快捷方式,以减少长路径的重复。在任何可以引用 crate::garden::vegetables::Asparagus 的作用域中,您都可以用 use crate::garden::vegetables::Asparagus; 创建快捷方式,此后您只需编写 Asparagus 就可以在作用域中使用该类型。 ===> 类似于C/C++中的using关键字;
在此,我们创建一个名为 backyard 的二进制crate来说明这些规则。crate 的目录也命名为 backyard ,其中包含这些文件和目录:
本例中的 crate 根文件是 src/main.rs,其中包含:
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("Hello, world!");
}
pub mod garden 告诉编译器包含garden,并在 src/garden.rs 中找到的代码:
pub mod vegetables;
这里, pub mod vegetables 意味着 src/garden/vegetables.rs 中的代码也包括在内。这些代码是:
#[derive(Debug)]
pub struct Asparagus {}
现在,让我们来详细了解这些规则,并在实际操作中加以演示!
7.2.2 在模块中分组相关代码
模块让我们可以在一个crate中组织代码,以提高可读性并方便重复使用。模块还允许我们控制项目的私密性,因为模块内的代码默认为私有。私有项目是内部实现细节,不对外公开。我们可以选择将模块和模块中的项目公开,这样就可以让外部代码使用和依赖它们。
例如,让我们编写一个提供餐厅功能的库。我们将定义函数的签名,但函数体留空,以便集中精力组织代码,而不是实现餐厅的功能。
在餐饮业,餐厅的某些部分被称为前厅,另一些部分被称为后厨。前厅是顾客所在的地方,包括主人为顾客安排座位、服务员接受点单和付款以及调酒师调制饮料的地方。后厨是厨师和厨工在厨房工作、洗碗工进行清洁以及经理进行行政工作的地方。
为了以这种方式构建我们的 crate,我们可以将其函数组织成嵌套模块。运行 cargo new restaurant --lib ,创建一个名为 restaurant 的新库;然后在 src/lib.rs 中输入如下代码,定义一些模块和函数签名。下面是前台部分:
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn server_order() {}
fn take_payment() {}
}
}
(清单 7-1: front_of_house 模块包含其他模块,这些模块又包含函数)
在定义模块时,我们使用 mod 关键字,然后是模块名称(本例中为 front_of_house )。模块的正文放在大括号内。在模块内部,我们可以放置其他模块,就像本例中的模块 hosting 和 serving 一样。模块还可以放置其他项目的定义,如结构体、枚举、常量、特质,以及清单 7-1 中的函数。
通过使用模块,我们可以将相关的定义组合在一起,并命名它们的关联原因。使用此代码的程序员可以根据分组来浏览代码,而不必通读所有定义,从而更容易找到与之相关的定义。程序员在为代码添加新功能时,也会知道该把代码放在哪里,从而使程序井井有条。
前面我们提到,src/main.rs 和 src/lib.rs 被称为 crate 根。之所以叫这两个文件的根,是因为这两个文件的内容构成了一个名为 crate 的模块,位于crate模块结构(即模块树)的根部。
下图显示了上述代码结构的模块树。
(清单 7-2:清单 7-1 中代码的模块树)
这棵树显示了一些模块是如何相互嵌套的;例如, hosting 嵌套在 front_of_house 中。这棵树还显示,有些模块是同级模块,即它们定义在同一个模块中; hosting 和 serving 是定义在 front_of_house 中的同级模块。如果模块 A 包含在模块 B 中,我们就说模块 A 是模块 B 的子模块,而模块 B 是模块 A 的父模块。请注意,整个模块树的根是名为 crate 的隐式模块。
模块树可能会让你联想到Linux系统上的文件系统目录树;这是一个非常恰当的比较!就像文件系统中的目录一样,你可以使用模块来组织代码。就像目录中的文件一样,我们需要找到模块的方法。
7.3 模块树中的项目引用路径
为了告诉 Rust 在模块树中的哪个位置可以找到某个项目,我们使用了路径,就像我们在文件系统中使用路径一样。要调用一个函数,我们需要知道它的路径。
路径有两种形式:
①. 绝对路径是从crate根开始的完整路径;对于外部crate的代码,绝对路径从crate名称开始,而对于当前crate的代码,绝对路径从字面 crate 开始。
②. 相对路径从当前模块开始,使用 self 、 super 或当前模块中的标识符。
绝对路径和相对路径后面都有一个或多个标识符,用双冒号 ( :: ) 分隔。
回到restaurant代码,假设我们要调用 add_to_waitlist 函数。这等同于问: add_to_waitlist 函数的路径是什么?
我们将展示两种从 crate root 中定义的新函数 eat_at_restaurant 调用 add_to_waitlist 函数的方法。这些路径都是正确的,但还存在另一个问题,会导致本示例无法按原样编译。稍后我们将解释原因。
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 server_order() {}
fn take_payment() {}
}
}
pub fn eat_at_restaurant( {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
})
(清单 7-3:使用绝对路径和相对路径调用 add_to_waitlist 函数)
第一次调用 eat_at_restaurant 中的 add_to_waitlist 函数时,我们使用的是绝对路径。 add_to_waitlist 函数与 eat_at_restaurant 定义在同一个 crate 中,这意味着我们可以使用 crate 关键字来启动绝对路径。然后,我们逐个包含后续模块,直到找到 add_to_waitlist 。你可以想象一个具有相同结构的文件系统:我们指定 /front_of_house/hosting/add_to_waitlist 路径来运行 add_to_waitlist 程序;使用 crate 名称从板块根目录启动,就像在 shell 中使用 / 从文件系统根目录启动一样。
第二次在 eat_at_restaurant 中调用 add_to_waitlist 时,我们使用了相对路径。路径以 front_of_house 开头,它是与 eat_at_restaurant 定义在模块树同级的模块名称。在这里,与文件系统相对应的路径是 front_of_house/hosting/add_to_waitlist 。以模块名开头意味着路径是相对的。
选择使用相对路径还是绝对路径取决于您的项目,也取决于您更倾向于将项目定义代码与使用项目的代码分开移动还是一起移动。例如,如果我们将 front_of_house 模块和 eat_at_restaurant 函数移到一个名为 customer_experience 的模块中,我们需要更新绝对路径到 add_to_waitlist ,但相对路径仍然有效。但是,如果我们将 eat_at_restaurant 函数单独移到一个名为 dining 的模块中,调用 add_to_waitlist 的绝对路径将保持不变,但相对路径需要更新。一般来说,我们更倾向于指定绝对路径,因为我们更有可能希望独立移动代码定义和项目调用。
让我们尝试编译清单 7-3,看看为什么还不能编译!我们得到如下的错误信息:
cargo.exe build
Compiling restaurant v0.1.0 (D:\rustProj\restaurant)
error[E0603]: module `hosting` is private
--> src\lib.rs:16:28
|
16 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| 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:19:21
|
19 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| 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` (lib) due to 2 previous errors
(清单 7-4:编译清单 7-3 中的代码时出现的编译器错误)
错误信息显示,模块 hosting 是私有的。换句话说,我们有 hosting 模块和 add_to_waitlist 函数的正确路径,但 Rust 不允许我们使用它们,因为它无法访问私有部分。在 Rust 中,所有项目(函数、方法、结构体、枚举、模块和常量)默认都是父模块私有的。如果你想将函数或结构体等项目私有化,就需要将其放入模块中。
父模块中的项目无法使用子模块中的私有项目,但子模块中的项目可以使用其祖先模块中的项目。这是因为子模块封装并隐藏了它们的实现细节,但子模块可以看到定义它们的上下文。继续我们的比喻,把隐私规则想象成一家餐厅的后台办公室:里面发生的事情对餐厅顾客来说是隐私,但办公室经理可以看到并做他们经营的餐厅里的一切事情。
Rust 选择让模块系统以这种方式运行,以便默认情况下隐藏内部实现细节。这样,你就可以知道在不破坏外层代码的情况下,你可以修改内部代码的哪些部分。不过,Rust 确实提供了一个选项,可以通过使用 pub 关键字将某项公开,从而向外部祖先模块公开子模块的内部代码。
7.3.1 使用 pub 关键字公开路径
让我们回到上述错误,该错误告诉我们 hosting 模块是私有的。我们希望父模块中的 eat_at_restaurant 函数能访问子模块中的 add_to_waitlist 函数,因此我们用 pub 关键字标记 hosting 模块,如下代码所示:
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn server_order() {}
fn take_payment() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
(清单 7-5:将 hosting 模块声明为 pub ,以便在以下情况下使用它 eat_at_restaurant)
不幸的是,如清单 7-6 所示,清单 7-5 中的代码仍会导致错误。
PS D:\rustProj\restaurant> cargo.exe build
Compiling restaurant v0.1.0 (D:\rustProj\restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src\lib.rs:16:37
|
16 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src\lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src\lib.rs:19:30
|
19 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src\lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
(清单 7-6:编译清单 7-5 中的代码时出现的编译器错误)
发生了什么?在 mod hosting 前面添加 pub 关键字后,模块就变成了公共模块。有了这个变化,如果我们能访问 front_of_house ,就能访问 hosting 。但是, hosting 的内容仍然是私有的;将模块公开并不会使其内容公开。模块上的 pub 关键字只能让其祖先模块中的代码引用它,而不能访问其内部代码。因为模块是一个容器,所以只将模块公开并不能做什么;我们需要进一步选择将模块中的一个或多个项也公开。
错误表明 add_to_waitlist 函数是私有的。隐私规则适用于结构体、枚举、函数和方法以及模块。
我们还可以在 add_to_waitlist 函数的定义前添加 pub 关键字,使其成为公共函数,如下所示:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn server_order() {}
fn take_payment() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
(清单 7-7:在 mod hosting 和 fn add_to_waitlist 中添加 pub 关键字后,我们就可以调用来自 eat_at_restaurant)
现在代码可以编译了!要了解为什么添加 pub 关键字能让我们在 add_to_waitlist 中使用这些路径,而不违反隐私规则,让我们来看看绝对路径和相对路径。
在绝对路径中,我们从 crate 开始,它是 crate 模块树的根。 front_of_house 模块定义在板块根中。虽然 front_of_house 并不公开,但由于 eat_at_restaurant 函数与 front_of_house 定义在同一个模块中(也就是说, eat_at_restaurant 和 front_of_house 是同胞兄弟),因此我们可以从 eat_at_restaurant 引用 front_of_house 。接下来是标有 pub 的 hosting 模块。我们可以访问 hosting 的父模块,因此可以访问 hosting 。最后, add_to_waitlist 函数被标记为 pub ,我们可以访问它的父模块,所以这个函数调用是有效的!
在相对路径中,逻辑与绝对路径相同,除了第一步:路径不是从crate根开始,而是从 front_of_house 开始。 front_of_house 模块与 eat_at_restaurant 定义在同一个模块中,因此从 eat_at_restaurant 所定义的模块开始的相对路径是有效的。然后,由于 hosting 和 add_to_waitlist 被标记为 pub ,因此路径的其余部分都有效,这个函数调用也就有效了!
如果你计划共享你的库板块,以便其他项目可以使用你的代码,那么你的公共 API 就是你与你的crate用户之间的契约,它决定了用户如何与你的代码交互。要管理公共 API 的变更,使人们更容易依赖你的 crate,需要考虑很多因素。
包含二进制文件和库的软件包的最佳实践
我们提到过,软件包可以同时包含 src/main.rs 二进制和 src/lib.rs 库,默认情况下这两个crate都使用软件包名称。通常情况下,同时包含库和二进制源码包的软件包会在二进制源码包中加入足够的代码,以启动调用库源码包代码的可执行文件。这样,其他项目就能从软件包提供的大部分功能中获益,因为库板块的代码可以共享。
模块树应在 src/lib.rs 中定义。然后,任何公共项目都可以通过以软件包名称开头的路径在二进制crate中使用。二进制crate将成为库crate的用户,就像完全外部板块使用库一样:它只能使用公共 API。这可以帮助你设计一个好的应用程序接口;你不仅是作者,也是客户!
7.3.2 用super启动相对路径
我们可以通过在路径开头使用 super 来构建从父模块而非当前模块或crate根开始的相对路径。这就像使用 .. 语法启动文件系统路径一样。使用 super ,我们就可以引用我们知道是在父模块中的项目,当模块与父模块关系密切,但父模块有一天可能会被移到模块树的其他地方时,这可以让重新排列模块树变得更容易。
请看如下代码,该代码模拟了厨师修改错误订单并亲自将其送到顾客手中的情况。 back_of_house 模块中定义的函数 fix_incorrect_order 调用父模块中定义的函数 deliver_order ,方法是指定从 super 开始的 deliver_order 的路径:
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
(清单 7-8:使用以 super)
fix_incorrect_order 函数位于 back_of_house 模块中,因此我们可以使用 super 进入 back_of_house 的父模块,在本例中就是 crate ,也就是根模块。从那里,我们查找 deliver_order 并找到它。成功!我们认为 back_of_house 模块和 deliver_order 函数很有可能保持相同的关系,如果我们决定重组crate的模块树,它们会被一起移动。因此,我们使用了 super ,这样,如果这些代码被移动到不同的模块,我们将来更新代码的地方就会减少。
7.3.3 公有结构体和枚举
我们也可以使用 pub 将结构体和枚举指定为公用,但在结构体和枚举中使用 pub 还有一些额外的细节。如果我们在结构体定义前使用 pub ,我们就会将结构体设为公共结构体,但结构体的字段仍然是私有的。我们可以根据具体情况决定是否公开每个字段。在下面代码示例中,我们定义了一个公共的 back_of_house::Breakfast 结构,它有一个公共的 toast 字段,但有一个私有的 seasonal_fruit 字段。这模拟了餐厅的情况:顾客可以选择配餐的面包类型,但厨师会根据当季和库存情况决定配餐的水果。现有的水果变化很快,所以顾客无法选择水果,甚至无法看到他们会吃到哪种水果。
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");
}
(清单 7-9:包含一些公共字段和一些私有字段的结构体)
由于 back_of_house::Breakfast 结构中的 toast 字段是公用的,因此在 eat_at_restaurant 中,我们可以使用点符号写入和读取 toast 字段。请注意,我们不能在 eat_at_restaurant 中使用 seasonal_fruit 字段,因为 seasonal_fruit 是私有的。试着取消修改 seasonal_fruit 字段值的行注释,看看会出现什么错误!
另外需要注意的是,由于 back_of_house::Breakfast 有一个私有字段,因此结构体需要提供一个公共关联函数来构造 Breakfast 的实例(我们在此将其命名为 summer )。如果 Breakfast 没有这样的函数,我们就无法在 eat_at_restaurant 中创建 Breakfast 的实例,因为我们无法在 eat_at_restaurant 中设置 seasonal_fruit 私有字段的值。
相反,如果我们将一个枚举公开,那么它的所有变体都是公开的。我们只需要在 enum 关键字前加上 pub ,如下代码所示:
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;
}
(清单 7-10:将枚举指定为公共枚举会使其所有变体都成为公共枚举)
由于我们将 Appetizer 枚举公开,因此可以在 eat_at_restaurant 中使用 Soup 和 Salad 变体。
除非枚举的变体是公开的,否则枚举的用处就不大;如果每次都要用 pub 来注解所有枚举变体,那就太烦人了,所以枚举变体的默认值是公开的。结构体通常在字段不公开的情况下也很有用,因此结构体字段遵循一般规则,即除非使用 pub 注释,否则默认情况下所有字段都是私有的。
还有一种涉及 pub 的情况我们没有涉及,那就是我们最后一个模块系统功能: use 关键字。我们将首先介绍 use 本身,然后演示如何将 pub 和 use 结合起来。
7.4 使用 use 关键字将路径纳入范围
必须写出调用函数的路径可能会让人感觉不便和重复。在上面代码中,无论我们选择的是 add_to_waitlist 函数的绝对路径还是相对路径,每次要调用 add_to_waitlist 时都必须指定 front_of_house 和 hosting 。幸运的是,有一种方法可以简化这一过程:我们可以使用 use 关键字创建一次路径快捷方式,然后在作用域中的其他地方使用较短的名称。
下面代码中,我们将 crate::front_of_house::hosting 模块引入 eat_at_restaurant 函数的作用域,这样我们只需指定 hosting::add_to_waitlist 就可以调用 eat_at_restaurant 中的 add_to_waitlist 函数。
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();
}
(清单 7-11:使用 use将模块加入作用域)
在作用域中添加 use 和路径类似于在文件系统中创建符号链接。在crate根目录中添加 use crate::front_of_house::hosting 后, hosting 就成为该作用域中的有效名称,就像在crate根目录中定义了 hosting 模块一样。使用 use 进入作用域的路径也会像其他路径一样检查隐私。
请注意, use 只为 use 出现的特定作用域创建快捷方式。下面代码示例将 eat_at_restaurant 函数移入一个名为 customer 的新子模块,该子模块的作用域与 use 语句不同,因此函数体将无法编译:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}
(清单 7-12: use 语句只适用于它所在的作用域)
编译器错误显示快捷方式不再适用于 customer 模块:
cargo.exe build
Compiling restaurant v0.1.0 (D:\rustProj\restaurant)
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
--> src\lib.rs:11:9
|
11 | hosting::add_to_waitlist();
| ^^^^^^^ use of undeclared crate or module `hosting`
|
help: consider importing this module through its public re-export
|
10 + use crate::hosting;
|
warning: unused import: `crate::front_of_house::hosting`
--> src\lib.rs:7:5
|
7 | use crate::front_of_house::hosting;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to previous error; 1 warning emitted
注意还有一个警告,即 use 在其作用域内已不再使用!要解决这个问题,请将 use 也移到 customer 模块中,或在子 customer 模块中用 super::hosting 引用父模块中的快捷方式。
7.4.1 创建惯用的使用路径
在上面代码中,您可能想知道为什么我们指定了 use crate::front_of_house::hosting ,然后在 eat_at_restaurant 中调用 hosting::add_to_waitlist ,而不是像如下代码示例中那样,为了达到同样的效果,一直指定 use 路径到 add_to_waitlist 函数。
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();
}
(清单 7-13:使用 use 将 add_to_waitlist 函数引入作用域,这不符合规范)
尽管都完成了相同的任务,但之前的代码是通过 use 将函数引入作用域的惯用方式:通过 use 将函数的父模块引入作用域意味着我们必须在调用函数时指定父模块。在调用函数时指定父模块可以清楚地表明函数不是本地定义的,同时还能最大限度地减少完整路径的重复。清单 7-13 中的代码不清楚 add_to_waitlist 的定义位置。
一方面,当使用 use 引入结构体、枚举和其他项目时,指定完整路径是一种习惯做法。下列代码展示了将标准库的 HashMap 结构引入二进制 crate 的作用域的惯用方法。
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
(清单 7-14:以惯用方式将 HashMap 引入作用域)
这个惯例的背后并没有很强的理由:它只是一种约定俗成的习惯,人们已经习惯了用这种方式来阅读和编写 Rust 代码。
如果我们使用 use 语句将两个同名的项目引入作用域,那么这个惯例就是个例外,因为 Rust 不允许这样做。下列代码展示了如何将两个具有相同名称但不同父模块的 Result 类型引入作用域,以及如何引用它们。
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
}
fn function2() -> io::Result<()> {
// --snip--
}
(清单 7-15:将两个同名类型带入同一作用域需要使用它们的父模块)
如您所见,使用父模块可以区分两个 Result 类型。如果我们指定的是 use std::fmt::Result 和 use std::io::Result ,那么在同一作用域中就会有两个 Result 类型,Rust 就不知道我们在使用 Result 时指的是哪一个了。
这个就是一个命名空间的问题,要保持函数命名的独一性;
7.4.2 使用 as 关键字提供新名称
对于将两个同名类型带入 use 的同一作用域的问题,还有另一种解决方案:在路径之后,我们可以指定 as 以及该类型的新本地名称或别名。清单 7-16 展示了清单 7-15 中代码的另一种写法,即使用 as 重命名两个 Result 类型中的一个。
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}
(清单 7-16:当使用 as 关键字将一个类型带入作用域时,对其进行重命名)
在第二个 use 语句中,我们为 std::io::Result 类型选择了新名称 IoResult ,这样就不会与我们也已纳入作用域的 std::fmt 中的 Result 冲突。
类似于C++中的using操作:
namespace MyNamespace {
class MyClass {};
}// Creating an alias for MyNamespace::MyClass
using MyClassAlias = MyNamespace::MyClass;// Now you can use MyClassAlias as a shorthand for MyNamespace::MyClass
MyClassAlias obj;
7.4.3 使用pub重新导出名称
当我们使用 use 关键字将名称引入作用域时,新作用域中的名称是私有的。为了使调用我们代码的代码能够引用该名称,就好像它是在该代码的作用域中定义的一样,我们可以将 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();
}
(清单 7-17:在一个新的作用域中,使用 pub use)
在这一变更之前,外部代码必须使用 restaurant::front_of_house::hosting::add_to_waitlist() 这一路径来调用 add_to_waitlist 函数。现在, pub use 已从根模块中重新导出了 hosting 模块,外部代码现在可以使用路径 restaurant::hosting::add_to_waitlist() 代替。
当代码的内部结构与调用代码的程序员对领域的理解不同时,重新导出就非常有用。例如,在这个餐馆隐喻中,经营餐馆的人考虑的是 "前厅 "和 "后厨"。但光顾餐厅的顾客可能不会用这些术语来思考餐厅的各个部分。有了 pub use ,我们就可以用一种结构编写代码,但暴露不同的结构。这样做可以使我们的程序库井井有条,便于程序员使用程序库,也便于程序员调用程序库。
7.4.4 使用外部Packages
在第 2 章中,,我们编写了一个竞猜游戏项目,该项目使用名为 rand 的外部软件包来获取随机数。为了在项目中使用 rand ,我们在 Cargo.toml 中添加了这一行:
rand = "0.8.5"
在 Cargo.toml 中将 rand 添加为依赖关系后,Cargo 就会从 crates.io 下载 rand 软件包和任何依赖关系,并将 rand 提供给我们的项目。
然后,为了将 rand 定义引入包的作用域,我们添加了一行以板条箱名称 rand 开头的 use 行,并列出了要引入作用域的项目。回想一下, "生成随机数 "一节中,我们将 Rng 特质引入了作用域,并调用了 rand::thread_rng 函数:
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..=100);
}
Rust 社区成员在 crates.io 上提供了许多软件包,将其中任何一个拉入你的软件包都需要这些相同的步骤:在你的软件包的 Cargo.toml 文件中列出它们,并使用 use 将项目从它们的crate拉入作用域。
请注意, std 标准库也是我们软件包外部的一个 crate。因为标准库是随 Rust 语言一起提供的,所以我们不需要修改 Cargo.toml 以包含 std 。但我们确实需要用 use 来引用它,以便将其中的项目引入我们软件包的范围。例如,在 HashMap 中,我们可以使用这一行:
use std::collections::HashMap;
这是一个以 std (标准库板块的名称)开头的绝对路径。
7.4.5 使用嵌套路径清理大型 use 列表
如果我们使用的是定义在同一crate或同一module中的多个项目,将每个项目单独列一行会占用文件中大量的垂直空间。例如,猜谜游戏中使用的这两条 use 语句将 std 中的项目引入作用域:
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--
相反,我们可以使用嵌套路径,在一行中将相同的项目引入作用域。具体做法是指定路径的共同部分,后面加两个冒号,然后在路径不同部分的列表周围加上大括号,如下代码所示:
// --snip--
use std::{cmp::Ordering, io};
// --snip--
(清单 7-18:指定嵌套路径,将具有相同前缀的多个项目纳入作用域)
在大型程序中,使用嵌套路径将同一板条箱或模块中的许多项目引入作用域,可以大大减少所需的单独 use 语句的数量!
我们可以在路径的任何层级使用嵌套路径,这在组合共享子路径的两个 use 语句时非常有用。例如,下列代码显示了两个 use 语句:一个将 std::io 带入作用域,另一个将 std::io::Write 带入作用域。
use std::io;
use std::io::Write;
(清单 7-19:两个 use 语句,其中一个是另一个的子路径)
这两条路径的共同部分是 std::io ,这就是完整的第一条路径。要将这两条路径合并为一条 use 语句,我们可以在嵌套路径中使用 self ,如下所示:
use std::io::{self, Write};
(清单 7-20:将清单 7-19 中的路径合并为一条 use 语句)
这一行将 std::io 和 std::io::Write 纳入范围。
7.4.6 Glob 操作
如果我们想将某个路径中定义的所有公共项都纳入作用域,可以指定该路径,然后使用 * glob 操作符:
use std::collections::*;
use 语句将 std::collections 中定义的所有公共项引入当前作用域。使用 glob 操作符时要小心!Glob 会使您更难分辨哪些名称在作用域中,以及程序中使用的名称是在哪里定义的。
测试时,glob 操作符通常用于将所有被测试的内容引入 tests 模块。glob 操作符有时也作为前奏模式的一部分使用。
7.5 将模块分成不同的文件
到目前为止,本章的所有示例都在一个文件中定义了多个模块。当模块变大时,你可能想把它们的定义移到一个单独的文件中,这样代码更容易浏览。
例如,让我们从清单 7-17 中包含多个餐厅模块的代码开始。我们将把模块提取到文件中,而不是把所有模块都定义在 crate 根文件中。在本例中,crate 根文件是 src/lib.rs,但这一过程也适用于二进制 crate,其 crate 根文件是 src/main.rs。
首先,我们将 front_of_house 模块提取到自己的文件中。删除 front_of_house 模块大括号内的代码,只留下 mod front_of_house; 声明,这样 src/lib.rs 文件就包含了清单 7-21 所示的代码。请注意,在创建清单 7-22 中的 src/front_of_house.rs 文件之前,这段代码不会被编译。
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
(清单 7-21:声明 front_of_house 模块,其主体将放在 src/front_of_house.rs 中)
接下来,将大括号中的代码放入一个名为 src/front_of_house.rs 的新文件中,如下代码所示。编译器之所以知道要在这个文件中查找,是因为它在 crate 根中找到了模块声明,其名称为 front_of_house 。
pub mod hosting {
pub fn add_to_waitlist() {}
}
(清单 7-22:src/front_of_house.rs 中 front_of_house 模块内的定义)
请注意,您只需在模块树中使用 mod 声明加载一次文件。一旦编译器知道该文件是项目的一部分(并且知道代码在模块树中的位置,因为你把 mod 语句放在了哪里),项目中的其他文件就应该使用 "在模块树中引用项目的路径 "一节中介绍的声明文件的路径来引用加载文件的代码。换句话说, mod 并不是你在其他编程语言中看到的 "包含 "操作。
接下来,我们将把 hosting 模块提取到它自己的文件中。这个过程有点不同,因为 hosting 是 front_of_house 的子模块,而不是根模块。我们将把 hosting 的文件放到一个新的目录中,该目录将以其在模块树中的祖先命名,本例中为 src/front_of_house/。
开始移动 hosting 时,我们修改 src/front_of_house.rs,使其仅包含 hosting 模块的声明:
pub mod hosting;
然后,我们创建一个 src/front_of_house 目录和一个 hosting.rs 文件,其中包含 hosting 模块中的定义:
pub fn add_to_waitlist() {}
如果我们把 hosting.rs 放在 src 目录下,编译器就会认为 hosting.rs 的代码是在 hosting 模块中,而不是作为 front_of_house 模块的子模块声明在 crate 根目录下。编译器会根据哪些文件检查哪些模块的代码,这意味着目录和文件更接近模块树。
备用文件路径
到目前为止,我们已经介绍了 Rust 编译器使用的最惯用的文件路径,但 Rust 还支持一种旧式的文件路径。对于在 crate 根目录中声明的名为 front_of_house 的模块,编译器会在以下目录中查找该模块的代码:
src/front_of_house.rs (我们所涉及的内容);
src/front_of_house/mod.rs(旧式,仍支持路径);
如果一个名为
hosting
的模块是front_of_house
的子模块,编译器会在以下位置查找该模块的代码:src/front_of_house/hosting.rs (我们涉及的内容)
src/front_of_house/hosting/mod.rs(旧式,仍支持路径)
如果在同一模块中同时使用两种样式,编译器会出错。对同一项目中的不同模块混合使用两种样式是允许的,但可能会让浏览项目的人感到困惑。
使用名为 mod.rs 的文件格式的主要缺点是,你的项目最终会有许多名为 mod.rs 的文件,当你同时在编辑器中打开这些文件时,可能会造成混乱。
我们将每个模块的代码都移到了单独的文件中,模块树保持不变。 eat_at_restaurant 中的函数调用无需任何修改即可运行,即使定义存在于不同的文件中。使用这种技术,可以随着模块规模的扩大,将它们移到新的文件中。
请注意,src/lib.rs 中的 pub use crate::front_of_house::hosting 语句也没有改变, use 也不会对作为 crate 一部分编译的文件产生任何影响。 mod 关键字声明了模块,Rust 会在与模块同名的文件中查找该模块的代码。
下一篇: 08-常用集合(容器)