Rust 具有许多功能,可供管理代码的组织,包括公开哪些细节,哪些细节是私有的,以及程序中每个范围内的名称。这些功能有时统称为模块系统,包括:
-
Packages: Cargo 的一项功能,可让您构建、测试和共享 crate
-
Crates:生成库或可执行文件的模块树
-
Modules and use:让您控制路径的组织、范围和隐私
-
Paths:
packages and crates
模块是 Rust 编译器在同一时间考虑的最小代码单元。即使你使用 rustc 而不是 cargo,并且仅传递一个源代码文件,编译器也会将该文件视为模块。模块可以包含其他模块,并且这些模块可以在随同模块一起编译的其他文件中定义。
模块可以以两种形式之一的形式出现:二进制模块或库模块。二进制模块是你可以编译成可执行文件的程序,例如命令行程序或服务器。每个二进制模块必须有一个名为 main 的函数,它定义了可执行文件运行时发生的事情。我们迄今为止创建的所有模块都是二进制模块。
库模块没有 main 函数,也不能编译成可执行文件。相反,它们定义的是旨在与多个项目共享的功能。例如,我们在第 2 章中使用的 rand 模块提供了生成随机数的功能。大多数时候,当 Rust 程序员说“模块”时,他们指的是库模块,并且将“模块”和一般的编程概念“库”交替使用。
包则是一组相关的模块的集合。包中的所有模块都共享同一个名称空间。包通常由多个文件组成,这些文件可能位于多个目录中。包还可以依赖于其他包,这意味着它们可以使用其他包中定义的功能。
在 Rust 中,每个文件都属于一个模块,而每个目录都属于一个包。在同一包中的所有文件都共享同一个名称空间。这意味着如果一个文件定义了一个函数或变量,那么这个函数或变量就可以在同一包中的其他文件中使用。
模块的根目录是 Rust 编译器从中启动的源文件,并构成了你的模块的根模块(我们将在“定义模块以控制作用域和隐私”一节中详细解释模块)。
包是一个或多个模块的集合,它提供了一组功能。包包含一个 Cargo.toml 文件,该文件描述了如何构建这些模块。Cargo 实际上是一个包,其中包含了你一直在使用的命令行工具的二进制模块。Cargo 包还包含了一个库模块,二进制模块依赖于此库模块。其他项目也可以依赖于 Cargo 库模块,以使用与 Cargo 命令行工具使用的相同逻辑。
包可以包含任意数量的二进制模块,但至多只能包含一个库模块(因为包的目的是提供一组功能,而不是多个不同的功能集。)。包必须至少包含一个模块,无论是库模块还是二进制模块。
使用 ls 查看 Cargo 创建了什么。在项目目录中,有一个 Cargo.toml 文件,提供了一个包。还有一个 src 目录,其中包含 main.rs。如果一个包只包含 src/main.rs,则意味着它只包含一个名为 my-project 的二进制模块。这意味着这个包只包含一个可执行文件,该文件的名称与包的名称相同(在这种情况下为 my-project)。
如果一个包同时包含 src/main.rs 和 src/lib.rs,则它有两个模块:一个名为 my-project 的二进制模块和一个同名的库模块。这意味着这个包包含一个可执行文件和一个库。
包可以通过将文件放在 src/bin 目录中拥有多个二进制模块:每个文件将是一个单独的二进制模块。这意味着你可以在同一个包中提供多个可执行文件,每个文件都有自己的名称。在这种情况下,你可以使用 Cargo 进行构建,并指定要构建哪个二进制模块。
一个包下多个二进制模块,一个库模块:
运行方式:
多个二进制模块时运行main
如果你的包中包含多个二进制模块,或者你希望运行一个名称不为 main.rs 的二进制模块,那么你需要指定二进制模块的名称。你可以使用 cargo build --bin 和 cargo run --bin 命令,其中 bin 后面跟着二进制模块的名称。
定义模块控制范围和隐私
模块系统是 Rust 中用来组织代码的一种机制。在本节中,我们将讨论模块以及模块系统的其他部分,包括允许你命名项目的路径;将路径引入作用域的 use 关键字;以及使项目变为公有的 pub 关键字。我们还将讨论 as 关键字、外部包和 glob 操作符。
crate root 通常是 src/lib.rs;对于二进制 crate,crate root 通常是 src/main.rs。如果你的包同时包含了 src/lib.rs 和 src/main.rs 两个文件,那么 src/lib.rs 是库模块的 crate root,src/main.rs 是二进制模块的 crate root。当你使用 cargo build 或者 cargo run 编译和运行这个包时,cargo 会根据你的命令来选择编译哪个 crate root。如果你使用 cargo build,cargo 会编译 src/lib.rs;如果你使用 cargo run,cargo 会编译 src/main.rs。
模块备忘录
以下提供了关于模块、路径、use 关键字和 pub 关键字在编译器中的工作方式,以及大多数开发人员如何组织代码的快速参考。
-
Start from the crate root: 编译crate时,编译器首先在crate根文件中查找要编译的代码(库crate一般是src/lib.rs, 对于二进制crate通常是src/main.rs)。
-
Declaring modules: 在crate根文件中,你可以声明新的模块。比如可以用 mod garden;来声明一个“garden”模块。编译器会在以下位置选择模块的代码: 1. 直接在mod garden后面定义,使用花括号替代分号。 1. 在文件src/garden.rs中 1. 在文件src/garden/mod.rs中。mod.rs 文件是 Rust 中的一种特殊文件。它允许你在一个文件夹中组织你的模块定义,并且在外部看起来就像一个普通的模块一样。使代码更容易阅读和维护。
-
Declaring submodules: 在crate root以外的任何文件中,都可以声明子模块。例如,你可以在src/garden.rs中声明 mod vegetables; 编译器将在以父模块命名的目录中查找子模块代码:
-
直接在mod vegetable后面,使用花括号替代分号。
-
文件src/garden/vegetables.rs
-
文件src/garden/vegetables/mod.rs
-
-
Paths to code in modules: 当一个模块成为你的 crate 的一部分时,你可以在 crate 中的任何地方引用该模块中的代码,只要隐私规则允许,使用该代码的路径。例如,在 garden vegetables 模块中的 Asparagus 类型将在 crate::garden::vegetables::Asparagus 处找到。
-
Private vs public: 默认情况下,模块内的代码是不能被它的父模块访问的。如果想让模块可以被父模块访问,就需要在模块定义时使用 pub mod 而不是 mod。如果想让公共模块内的项也可以被其他地方访问,需要在项的定义前加上 pub 关键字。
-
The use keyword: 在一个范围中,使用关键字可以为项目创建快捷方式,以减少长路径的重复。在任何可以引用crate::garden::vegetables::Asparagus的范围中,您可以使用 use crate::garden::vegetables::Asparagus; 创建一个快捷方式,从此以后,只需要编写 Asparagus 即可在该范围中使用该类型。
引用其他模块:
vegetables.rs代码:
pub struct Asparagus {}
pub fn hello_world(){
println!("hello world vegetables!!");
}
garden.rs代码:
pub mod vegetables;
pub fn hello_world(){
println!("hello garden");
}
main.rs代码:
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus{};
garden::hello_world();
crate::garden::vegetables::hello_world();
garden::vegetables::hello_world();
println!("I'm growing {:?}!", plant);
}
bin目录一般用于存放可执行文件,也就是项目中的主程序。这些文件都是独立的可执行文件,不需要其它文件的支持。
如果你把garden.rs放在bin目录下,那么garden.rs就不是项目的crate root了,因此在编译时不会被编译,也就不能在main中直接使用了。你需要将garden.rs中的代码移动到src目录下,并修改mod garden引入路径。指定cargo run --bin backyard 是为了运行bin目录下的main.rs。
故而在garden.rs写main函数永远不会执行,src/main.rs是默认的crate root文件。
使用mod.rs的好处。
将相关代码分组到模块中
在Rust中,模块(module)提供了一种将代码在crate中进行组织和重复使用的方式。模块还允许我们控制项目的隐私,因为模块内的代码默认是私有的。私有项目是外部代码不能使用的实现细节。我们可以选择将模块和其中的项目公开,这样就可以将它们暴露给外部代码使用和依赖。
餐厅库的定义:
前台模块的实现:
我们使用关键字 mod 和模块的名称(在这种情况下是 front_of_house)来定义一个模块。然后将模块的正文放在大括号内。在模块中,我们可以放其他模块,如在这种情况下的 hosting 和 serving 模块。模块还可以包含其他项目的定义,如结构体、枚举、常量、特征,以及函数。
通过使用模块,我们可以将相关的定义分组并命名它们的关系。使用此代码的程序员可以根据组来导航代码,而不是必须阅读所有定义,这样可以更容易地找到对他们有关的定义。向此代码添加新功能的程序员将知道将代码放置在哪里以保持程序的组织性。
先前,我们提到 src/main.rs 和 src/lib.rs 称为 crate roots。它们名称的原因是这两个文件中的内容形成了一个名为 crate 的模块,该模块位于 crate 模块结构的根部,被称为模块树。请注意,整个模块树都以名为 crate 的隐式模块为根。
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
路径的两种形式:
-
绝对路径是从 crate 根开始的完整路径;对于来自外部 crate 的代码,绝对路径以 crate 名称开头,对于来自当前 crate 的代码,它从字面量 crate 开始。
-
相对路径从当前模块开始,并使用 self,super 或当前模块中的标识符。
无论是绝对路径还是相对路径,都要由一个或多个由双冒号(::)分隔的标识符来提示。
选择是使用相对路径还是绝对路径是您将根据您的项目做出的决定,并且取决于您是否更有可能将项目定义代码与使用该项目的代码分开移动或一起移动。例如,如果我们将front_of_house
模块和eat_at_restaurant
函数移动到名为 的模块customer_experience
中,我们需要将绝对路径更新为add_to_waitlist
,但相对路径仍然有效。但是,如果我们将eat_at_restaurant
函数单独移动到名为 的模块dining
中,调用的绝对路径add_to_waitlist
将保持不变,但相对路径需要更新。
我们通常倾向于指定绝对路径,因为我们更有可能希望彼此独立地移动代码定义和项目调用。
在 Rust 中,所有项目(函数,方法,结构体,枚举,模块和常量)默认对父模块私有。如果你想让一个像函数或结构体的项目私有,可以把它放在一个模块中。
pub关键字
pub
在前面添加关键字mod hosting
使模块公开。通过此更改,如果我们可以访问front_of_house
,我们就可以访问hosting
。但内容hosting
仍然是私有的;公开模块并不会公开其内容。模块上的pub
关键字只允许其祖先模块中的代码引用它,而不能访问其内部代码。因为模块是容器,所以我们只能通过公开模块来做很多事情;我们需要更进一步,选择公开模块中的一项或多项。
super实现相对路径
我们可以使用 super 在路径开头构建相对路径,而不是在当前模块或 crate 根中构建。这类似于使用 .. 语法开始文件系统路径。使用 super 允许我们引用一个我们知道在父模块中的项目,这可以使重新排列模块树变得更容易,因为当模块与父模块密切相关,但父模块可能会在未来的模块树中移动。
fix_incorrect_order 函数在 back_of_house 模块中,所以我们可以使用 super 去到 back_of_house 的父模块,在这种情况下是根模块 crate。然后,我们查找 deliver_order 并找到它。
公开结构和枚举
我们还可以使用 pub 来指定结构体和枚举为公共的,但是使用 pub 与结构体和枚举有一些额外的细节。如果在结构体定义前使用 pub,则使结构体公共,但结构体的字段仍将是私有的。我们可以根据情况分别对每个字段进行公开或不公开。
需要注意的是,由于 back_of_house::Breakfast 具有私有字段,因此该结构体需要提供一个公共关联函数来构造 Breakfast 的实例(我们在这里将其命名为 summer)。如果 Breakfast 没有这样的函数,我们就不能在 eat_at_restaurant 中创建 Breakfast 的实例,因为我们无法在 eat_at_restaurant 中设置私有 seasonal_fruit 字段的值。
公开枚举
如果我们公开一个枚举,那么它的所有变体都是公开的(字段都是公开的)。我们只需要pub
在enum
关键字之前。
如果枚举的变量不是公共的,那么它们将没有用处;在每种情况下都要用 pub 注释所有枚举变量将是麻烦的,因此枚举变量的默认值是公共的。结构体在没有公共字段的情况下往往很有用,因此结构体字段遵循一般规则,即除非使用 pub 注释,否则一切都是私有的。
使用use关键字创建路径,使范围内
使用 use 和路径在作用域中类似于文件系统中创建一个符号链接。 通过在 crate 根中添加 use crate::front_of_house::hosting,hosting 现在是该作用域中有效的名称,就像 hosting 模块已在 crate 根中定义一样。 使用的路径也会检查隐私,就像其他路径一样。
use 只在特定作用域中创建了快捷方式。将 eat_at_restaurant 函数移动到新的子模块 customer 中,然后它是不同的作用域,因此函数体将不能编译。
该警告表示该 use 在其作用域中不再使用! 要解决此问题,请将 use 移动到 customer 模块中,或在 child customer 模块中使用 super::hosting 引用父模块中的快捷方式。
在使用引入结构、枚举和其他项时use
,惯用的做法是指定完整路径。
处理相同名称问题
不用as:
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
Ok(())
}
使用as处理相同名称
pub use (未实践)
当我们使用 use 关键字将名称带入作用域时,新作用域中可用的名称是私有的。 为了使调用我们代码的代码能够像在其作用域中定义了该名称一样引用该名称,我们可以结合 pub 和 use。 此技术称为重新导出,因为我们正在将项目带入作用域,并使该项目可供其他人带入其作用域。
pub use
语句主要用于控制您的crate的公共接口,通过重新导出某些项目并使它们更易于访问crate的用户。这可以使API更方便和用户友好,因为它减少了访问某些项目所需的嵌套层数,并允许用户以与crate的内部结构不同的方式思考领域。
-
use
关键字是将某个路径中的名称引入当前作用域,使之在当前作用域内直接可用。 -
pub use
关键字是将某个路径中的名称引入当前作用域并且标记为公共,使之在当前作用域外也可用。
在此更改之前,外部代码调用该函数 restaurant::front_of_house::hosting::add_to_waitlist()
。现在pub use
已经从根模块中重新导出了hosting
模块,外部代码现在可以改用该路径restaurant::hosting::add_to_waitlist()
。
使用外部包
同第二章猜谜游戏。
1. 在Cargo.toml 文件内添加 dependencies、
-
将
Rng
特征带入作用域并调用rand::thread_rng
函数:
请注意,标准std
库也是我们包外部的一个板条箱。因为标准库是随 Rust 语言一起提供的,所以我们不需要更改Cargo.toml来包含std
。但是我们确实需要引用它use
以将项目从那里带入我们的包范围。例如,HashMap
我们将使用这一行:
fn main() {
use std::collections::HashMap;
}
解决相同crate重复use问题
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--
修改为:
// --snip--
use std::{cmp::Ordering, io};
// --snip--
当两个use一个为另一个子路径时
use std::io;
use std::io::Write;
可写成:
use std::io::{self, Write};
*操作符
如果我们想要将路径中定义的所有公共项目都带入作用域中,我们可以指定该路径,然后跟着 * 通配符:
fn main() {
use std::collections::*;
}
该use
语句将 中定义的所有公共项目std::collections
带入当前范围。使用 glob 运算符时要小心!Glob 可以让你更难分辨哪些名字在范围内,以及在你的程序中使用的名字是在哪里定义的。
测试时经常使用 glob 运算符,将所有被测试的东西都带入tests
模块;
拆分mod为不同文件
注意,您只需要在模块树中使用 mod 声明加载一次文件。一旦编译器知道文件是项目的一部分(并且因为您放置 mod 语句的位置而知道代码在模块树中的位置),项目中的其他文件应该使用声明中的路径来引用已加载文件的代码,如“在模块树中引用项目的路径”部分所述。换句话说,mod 不是其他编程语言中可能看到的“include”操作(在每个文件中都需要使用 include 来引入头文件。)。
接下来,我们将提取托管模块到其自己的文件中。这个过程有点不同,因为托管是 front_of_house 的子模块,而不是根模块。我们将托管文件放在一个新目录中,该目录将命名为其模块树中祖先的名称,在这种情况下为 src/front_of_house/。
接着拆分hosting
备用文件路径
到目前为止,我们已经介绍了 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 的文件,在编辑器中同时打开它们时可能会感到困惑。
将模块代码移动到单独的文件中, 并保持模块树不变. 这样做可以让代码更容易维护和扩展, 当模块变得越来越大时可以将它们移动到新的文件中。
pub use crate::front_of_house::hosting 语句在src/lib.rs中, 表示将 crate::front_of_house::hosting 模块暴露在crate根作用域下, 其他模块可以直接使用hosting模块中的函数.
use 关键字并不影响编译器编译文件, 它只是让编译器知道这个模块在哪里, 以便在编译时能够找到这个模块. mod关键字声明了模块,并且Rust会在与模块同名的文件中查找该模块的代码.
总结
文章评论