模块
Table of Contents
当编写大型程序时,组织代码显得尤为重要,因为想在脑海中通晓整个程序,那几乎是不可能完成的。通过对 相关功能 进行 分组 和 划分 不同功能的代码,可以清楚 在哪里可以找到实现了特定功能的代码 ,以及在 哪里可以改变一个功能 的工作方式
到目前为止,我们编写的程序都在一个文件的一个模块中 伴随着项目的增长,可以通过将代码分解为多个模块和多个文件来组织代码: 一个包可以包含多个二进制 crate 项和一个可选的 crate 库 伴随着包的增长,还可以将包中的部分代码提取出来,做成独立的 crate,这些 crate 则作为外部依赖项
除了对功能进行分组以外, 封装实现细节 可以更高级地 重用代码 :实现了一个操作后,其他的代码可以通过该代码的公共接口来进行调用,而不需要知道它是如何实现的
在编写代码时可以定义哪些部分是其他代码可以使用的公共部分,以及哪些部分是有权更改实现细节的私有部分 这是另一种减少脑海中记住项目内容数量的方法
这里有一个需要说明的概念 作用域 : 代码所在的嵌套上下文 有一组定义为 in scope 的 名称 。当阅读、编写和编译代码时,程序员和编译器需要知道 特定位置的特定名称 是否引用了 变量 、 函数 、 结构体 、 枚举 、 模块 、 常量 或者 其他有意义的项
可以创建作用域,以及改变哪些名称在作用域内还是作用域外 同一个作用域内不能拥有两个相同名称的项 我们可以使用一些工具来解决名称冲突
Rust 有许多功能可以让管理代码的组织,包括哪些内容可以被公开,哪些内容作为私有部分,以及程序每个作用域中的名字。这些功能。被称为 模块系统 ,包括:
- 包 : Cargo 的一个功能,它允许你 构建 、 测试 和 分享 crate
- Crates :一个 模块的树形结构 ,它形成了 库 或 二进制项目
- 模块 和 use : 允许你控制 作用域 和 路径 的 私有性
- 路径 :一个命名例如 结构体 、 函数 或 模块 等项的方式
本章将会涵盖所有这些概念,讨论它们如何交互,并说明如何使用它们来管理作用域 到最后,会对模块系统有深入的了解,并且能够像专业人士一样使用作用域! 对于一个由一系列相互关联的包组合而成的超大型项目,Cargo 还提供了 “工作空间” 这一功能,本章暂时不做讲解
包和 crate
crate 是一个 二进制项 或者 库 :
- crate root 是一个 源文件 :Rust 编译器以它为起始点,并构成 crate 的 根模块
- 包 :提供一系列功能的 一个或者多个 crate
- 一个包会包含有一个 Cargo.toml 文件,阐述如何去 构建这些 crate
包中所包含的内容由几条规则来确立:
- 一个包中 至多只能 包含 一个库 crate
- 包中可以包含 任意 多个 二进制 crate
- 包中 至少 包含 一个 crate ,无论是库的还是二进制的
当输入命令 cargo new:
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
Cargo 会给我们的包创建一个 Cargo.toml 文件
root
查看 Cargo.toml 的内容,却会发现并没有提到 src/main.rs
因为 Cargo 遵循的一个约定:
- src/main.rs : 就是一个与 包同名 的 二进制 crate 的 crate 根
- 如果包目录中包含 src/lib.rs ,则包带有与其 同名 的 库 crate ,且 src/lib.rs 是 crate 根
- crate 根文件将由 Cargo 传递给 rustc 来实际构建库或者二进制项目
至此,已经有了一个只包含 src/main.rs 的包,意味着它只含有一个名为 my-project 的二进制 crate 如果一个包同时含有 src/main.rs 和 src/lib.rs,则它有两个 crate:一个库和一个二进制项,且名字都与包相同
通过将文件放在 src/bin 目录下,一个包可以 拥有多个二进制 crate :每个 src/bin 下的文件都会被编译成一个独立的二进制 crate
作用
一个 crate 会将一个作用域内的相关功能分组到一起,使得该功能可以很方便地在 多个项目之间共享
举一个例子,曾经使用的 rand crate 提供了生成随机数的功能 通过将 rand crate 加入到我们项目的作用域中,就可以在自己的项目中使用该功能 rand crate 提供的所有功能都可以通过该 crate 的名字:rand 进行访问
将一个 crate 的功能保持在其自身的作用域中,可以知晓一些特定的功能是在自己的 crate 中定义的还是在 rand crate 中定义的,这可以 防止潜在的冲突
例如,rand crate 提供了一个名为 Rng 的特性(trait),还可以在自己的 crate 中定义一个名为 Rng 的 struct 因为一个 crate 的功能是在自身的作用域进行命名的,所以编译器会知道: Rng 这个名字指向的是我们自己定义的 struct Rng rand::Rng 这一方式指向的才是 rand crate 中的 Rng 特性
模块
在餐饮业,餐馆中会有一些地方被称之为前台,还有另外一些地方被称之为后台 前台:招待顾客的地方,在这里,店主可以为顾客安排座位,服务员接受顾客下单和付款,调酒师会制作饮品 后台:由厨师工作的厨房,洗碗工的工作地点,以及经理做行政工作的地方组成
可以将 函数 放置 到 嵌套的模块 中,来使 crate 结构与实际的餐厅结构相同。通过执行 cargo new –lib restaurant ,来创建一个新的名为 restaurant 的库
cargo new --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() {} } }
定义一个模块 ,是以 mod 关键字为起始,然后指定 模块的名字 (本例中叫做 front_of_house),并且用 花括号 包围模块的主体
- 在模块内,还可以 定义 其他的模块 ,就像本例中的 hosting 和 serving 模块
- 模块还可以 保存 一些定义的其他项,比如 结构体 、 枚举 、 常量 、 特性 、或者 函数
通过使用模块,可以将相关的定义分组到一起,并指出他们为什么相关 程序员可以通过使用这段代码,更加容易地找到他们想要的定义,因为他们可以基于分组来对代码进行导航,而不需要阅读所有的定义 程序员向这段代码中添加一个新的功能时,他们也会知道代码应该放置在何处,可以保持程序的组织性
- 模块可以将一个 crate 中的 代码分组 ,以提高 可读性 与 重用性
- 模块还可以控制项的 私有性 :
- public: 项是可以被 外部代码使用的
- private: 作为一个 内部实现 的内容,不能被外部代码使用
模块树
在前面提到了, src/main.rs 和 src/lib.rs 叫做 crate 根 。之所以这样叫它们的原因是:这两个文件的内容都是一个从名为 crate 的模块作为 根的 crate 模块结构 ,称为 模块树 :
crate └── front_of_house ├── hosting │ ├── add_to_waitlist │ └── seat_at_table └── serving ├── take_order ├── serve_order └── take_payment
- 这个树展示了一些模块是如何被嵌入到另一个模块的:例如,hosting 嵌套在 front_of_house 中
- 这个树还展示了一些模块是互为 兄弟 的,这意味着它们定义在同一模块中:hosting 和 serving 被一起定义在 front_of_house 中
- 如果一个模块 A 被包含在模块 B 中,将模块 A 称为模块 B 的 子 ,模块 B 则是模块 A 的 父
- 注意:整个模块树都植根于名为 crate 的 隐式模块 下
这个模块树可能会令你想起电脑上文件系统的目录树:这是一个非常恰当的比喻! 就像文件系统的目录,可以使用模块来组织代码,并且,就像目录中的文件,需要一种方法来找到模块
路径
来看一下 Rust 如何在模块树中找到一个项的位置:Rust 会使用路径的方式,就像在文件系统使用路径一样 如果想要调用一个函数,就需要知道它的路径
路径 有两种形式:
- 绝对 路径:从 crate 根 开始,以 crate 名 或者 字面值 crate 开头
- 相对 路径:从 当前模块 开始,以 self 、 super 或 当前模块的标识符 开头
绝对路径和相对路径都需要后面跟一个或多个由双冒号 :: 分割的标识符
回到开始的示例:如何调用 add_to_waitlist 函数? 还是同样的问题,add_to_waitlist 函数的路径是什么?
下面例子里通过删除一些模块和函数,稍微简化了一下代码。在 crate 根定义了一个新函数 eat_at_restaurant,并在其中展示调用 add_to_waitlist 函数的两种方法:
- eat_at_restaurant 函数是我们 crate 库的一个 公共 API,所以使用 pub 关键字来标记它
mod front_of_house { mod hosting { 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(); }
第一种方式:在 eat_at_restaurant 中调用 add_to_waitlist 函数,使用的是 绝对 路径
- add_to_waitlist 函数与 eat_at_restaurant 被定义在同一 crate 中,这意味着可以使用 crate 关键字为起始的 绝对 路径
在 crate 后面,持续地嵌入模块,直到找到 add_to_waitlist 可以想象出一个相同结构的文件系统,通过指定路径 /front_of_house/hosting/add_to_waitlist 来执行 add_to_waitlist 程序 我们使用 crate 从 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,但是相对路径必须要更新 一般更倾向于使用绝对路径,因为它更适合移动代码定义和项调用的相互独立
试着编译一下,却发现无法编译通过:
$ 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(); | ^^^^^^^ error[E0603]: module `hosting` is private --> src/lib.rs:12:21 | 12 | front_of_house::hosting::add_to_waitlist(); | ^^^^^^^
错误信息说 hosting 模块是 私有 的
换句话说,虽然拥有 hosting 模块和 add_to_waitlist 函数的的正确路径,但是 Rust 不让使用,因为它不能访问私有片段
模块不仅对于 组织代码 很有用。他们还定义了 Rust 的 私有性边界 :这条界线不允许外部代码了解、调用和依赖被封装的实现细节
所以,如果希望创建一个私有函数或结构体,可以将其放入模块
- Rust 中默认所有项( 函数 、 方法 、 结构体 、 枚举 、 模块 和 常量 )都是 私有 的
- 父模块 中的项 不能 使用 子模块 中的 私有项
- 但是 子模块 中的项 可以 使用他们 父模块 中的项
这是因为子模块封装并隐藏了他们的实现详情,但是子模块可以看到他们定义的上下文 继续拿餐馆作比喻,把私有性规则想象成餐馆的后台办公室:餐馆内的事务对餐厅顾客来说是不可知的,但办公室经理可以洞悉其经营的餐厅并在其中做任何事情 因此Rust默认会隐藏内部实现细节:这样一来,就知道可以更改内部代码的哪些部分而不会破坏外部代码 当然还可以通过使用 pub 关键字来创建公共项,使子模块的内部部分暴露给上级模块
使用 pub 关键字暴露路径
回头看一下前面示例的错误,这次使用 pub 关键字来标记 hosting 模块,如下面所示:
mod front_of_house { pub mod hosting { 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(); }
不幸的是,上面的代码编译仍然有错误:
$ cargo build Compiling restaurant v0.1.0 (file:///projects/restaurant) error[E0603]: function `add_to_waitlist` is private --> src/lib.rs:9:37 | 9 | crate::front_of_house::hosting::add_to_waitlist(); | ^^^^^^^^^^^^^^^ error[E0603]: function `add_to_waitlist` is private --> src/lib.rs:12:30 | 12 | front_of_house::hosting::add_to_waitlist(); | ^^^^^^^^^^^^^^^
在 mod hosting 前添加了 pub 关键字,使其变成公有的 伴随着这种变化,如果可以访问 front_of_house,那也可以访问 hosting 但是 hosting 的内容仍然是私有的;这表明使模块公有并不使其内容也是公有的 模块上的 pub 关键字只允许其父模块引用它,因此私有性规则不但应用于模块,还应用于结构体、枚举、函数和方法
继续将 pub 关键字放置在 add_to_waitlist 函数的定义之前,使其变成公有:
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 中调用这些路径 在绝对路径,从 crate,也就是 crate 根开始: 然后 crate 根中定义了 front_of_house 模块: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,路径其余的部分也是有效的,因此函数调用也是有效的!
使用 super 起始的相对路径
还可以使用 super 开头来构建从父模块开始的相对路径。这么做类似于文件系统中以 .. 开头的语法
考虑一下下面示例的代码,它模拟了厨师更正了一个错误订单,并亲自将其提供给客户的情况。fix_incorrect_order 函数通过指定的 super 起始的 server_order 路径,来调用 server_order 函数:
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 父模块,也就是本例中的 crate 根,在这里可以找到 serve_order 这里我们认为 back_of_house 模块和 server_order 函数之间可能具有某种关联关系,并且,如果要重新组织这个 crate 的模块树,需要一起移动它们 因此,使用 super,这样一来,如果这些代码被移动到了其他模块,只需要更新很少的代码
创建公有的结构体和枚举
以使用 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"); }
- 因为 back_of_house::Breakfast 结构体的 toast 字段是公有的,所以可以在 eat_at_restaurant 中使用点号来随意的读写 toast 字段
- 不能在 eat_at_restaurant 中使用 seasonal_fruit 字段,因为 seasonal_fruit 是私有的
尝试去除那一行修改 seasonal_fruit 字段值的代码的注释,就会得到下面的报错:
error[E0616]: field `seasonal_fruit` of struct `back_of_house::Breakfast` is private --> src/lib.rs:27:5 | 27 | meal.seasonal_fruit = String::from("blueberries"); | ^^^^^^^^^^^^^^^^^^^
还请注意一点:因为 back_of_house::Breakfast 具有私有字段,所以这个结构体需要提供一个 公共的 关联函数 来 构造 示例 Breakfast :这里命名为 summer
如果 Breakfast 没有这样的函数,将无法在 eat_at_restaurant 中创建 Breakfast 实例,因为不能在 eat_at_restaurant 中设置私有字段 seasonal_fruit 的值
与之相反:如果将 枚举 设为 公有 ,则它的 所有成员都将变为公有 ,只需要在 enum 关键字前面加上 pub :
结构体通常使用时,不必将它们的字段公有化,因此结构体遵循常规,内容全部是私有的,除非使用 pub 关键字 如果枚举成员不是公有的,那么枚举会显得用处不大;给枚举的所有成员挨个添加 pub 是很令人恼火的,因此枚举成员默认就是公有的
下面创建了名为 Appetizer 的 公有 枚举,就可以在 eat_at_restaurant 中使用 Soup 和 Salad 成员:
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; }
还有一种使用 pub 的场景还没有涉及到,那就是最后要讲的模块功能:use 关键字 将先单独介绍 use,然后展示如何结合使用 pub 和 use
use 导入模块
到目前为止,似乎编写的用于调用函数的路径都很冗长且重复,并不方便 例如,无论选择 add_to_waitlist 函数的绝对路径还是相对路径,每次想要调用 add_to_waitlist 时,都必须指定front_of_house 和 hosting 幸运的是,有一种方法可以简化这个过程。可以一次性将路径引入作用域,然后使用 use 关键字调用该路径中的项,就如同它们是本地项一样
使用 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(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); }
在作用域中增加 use 和路径类似于在文件系统中创建符号连接 通过在 crate 根增加 use crate::front_of_house::hosting,现在 hosting 在作用域中就是有效的名称了,如同 hosting 模块被定义于 crate 根一样 通过 use 引入作用域的路径也会检查私有性,同其它路径一样
还可以使用 use 和 相对 路径来将一个项引入作用域:
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use 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 ,然后在 eat_at_restaurant 中调用 hosting::add_to_waitlist ,而不是通过指定一直到 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(); }
虽然上面示例也完成了相同的任务,但这样的代码不清楚 add_to_waitlist 究竟是在哪里被定义的 相比较而言,而在前面实例中,如果想使用 use 将函数的父模块引入作用域,必须在调用函数时指定父模块,这样可以清晰地表明函数不是在本地定义的,同时使完整路径的重复度最小化
另一方面,使用 use 引入 结构体 、 枚举 和 其他项 时,习惯是指定它们的 完整路径 。下面展示了将 HashMap 结构体引入二进制 crate 作用域的习惯用法:
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
这种习惯用法背后没有什么硬性要求:它只是一种惯例,人们已经习惯了以这种方式阅读和编写 Rust 代码
这个习惯用法有一个例外,那就是想使用 use 语句将 两个具有相同名称的项 带入作用域,因为 Rust 不允许这样做。下面展示了如何将两个具有相同名称但不同父模块的 Result 类型引入作用域,以及如何引用它们:
use std::fmt; use std::io; fn function1() -> fmt::Result { // --snip-- } fn function2() -> io::Result<()> { // --snip-- }
如你所见,使用父模块可以区分这两个 Result 类型 如果指定 use std::fmt::Result 和 use std::io::Result,将在同一作用域拥有了两个 Result 类型,当使用 Result 时,Rust 则不知道我们要用的是哪个
使用 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 的新名称,它与从 std::fmt 引入作用域的 Result 并不冲突 这与前一个实例的用法都是惯用的,如何选择都取决于你!
使用 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(); }
- 通过 pub use,现在可以通过新路径 hosting::add_to_waitlist 来调用 add_to_waitlist 函数
- 如果没有指定 pub use: eat_at_restaurant 函数可以在其作用域中调用 hosting::add_to_waitlist,但外部代码则不允许使用这个新路径
当你的代码的内部结构与调用你的代码的程序员的思考领域不同时,重导出会很有用 例如,在这个餐馆的比喻中,经营餐馆的人会想到“前台”和“后台”,但顾客在光顾一家餐馆时,可能不会以这些术语来考虑餐馆的各个部分 使用 pub use,可以使用一种结构编写代码,却将不同的结构形式暴露出来。这样做使我们的库井井有条,方便开发这个库的程序员和调用这个库的程序员之间组织起来
使用外部包
前面的猜猜看游戏。那个项目使用了一个外部包, rand 来生成随机数。为了在项目中使用 rand,在 Cargo.toml 中加入了如下行:
[dependencies] rand = "0.5.5"
在 Cargo.toml 中加入 rand 依赖告诉了 Cargo 要从 crates.io 下载 rand 和其依赖,并使其可在项目代码中使用
为了将 rand 定义引入项目包的作用域,加入一行 use 起始的包名,它以 rand 包名 开头并列出了需要 引入作用域的项 :
use rand::Rng; fn main() { let secret_number = rand::thread_rng().gen_range(1, 101); }
crates.io 上有很多 Rust 社区成员发布的包,将其引入你自己的项目都需要一道相同的步骤:在 Cargo.toml 列出它们并通过 use 将其中定义的项引入项目包的作用域中
注意: 标准库 std 对于你的包来说也是 外部 crate。因为标准库随 Rust 语言一同分发,无需修改 Cargo.toml 来引入 std,不过需要通过 use 将标准库中定义的项引入项目包的作用域中来引用它们,比如使用的 HashMap:
use std::collections::HashMap;
这是一个以标准库 crate 名 std 开头的绝对路径
嵌套路径来消除大量的 use 行
当需要引入很多定义于 相同包 或 相同模块 的项时,为每一项单独列出一行会占用源码很大的空间。例如:
use std::cmp::Ordering; use std::io; // ---snip---
可以使用 嵌套 路径将相同的项在一行中引入作用域。这么做需要指定 路径的相同部分 ,接着是 两个冒号 ,接着是 大括号 中的各自 不同的路径部分 ,如下所示:
use std::{cmp::Ordering, io};
在较大的程序中,使用嵌套路径从相同包或模块中引入很多项,可以显著减少所需的独立 use 语句的数量!
可以在路径的任何层级使用嵌套路径,这在 组合 两个 共享子路径 的 use 语句时非常有用。例如,下面展示了两个 use 语句:一个将 std::io 引入作用域,另一个将 std::io::Write 引入作用域:
use std::io; use std::io::Write;
两个路径的相同部分是 std::io ,这正是第一个路径。为了在一行 use 语句中引入这两个路径,可以在嵌套路径中使用 self :
use std::io::{self, Write};
这便将 std::io 和 std::io::Write 同时引入作用域
通过 * 运算符将所有的公有定义引入作用域
如果希望将一个路径下 所有 公有项 引入作用域,可以指定路径后跟 * 运算符:
use std::collections::*;
这个 use 语句将 std::collections 中定义的所有公有项引入当前作用域
使用 glob 运算符时请多加小心:它会使得难以推导作用域中有什么名称和它们是在何处定义的 glob 运算符经常用于测试模块 tests 中,这时会将所有内容引入作用域,有时也用于 prelude 模式
将模块分割进不同文件
到目前为止,所有的例子都在一个文件中定义多个模块 当模块变得更大时,可能想要将它们的定义移动到单独的文件中,从而使代码更容易阅读
例如:将 front_of_house 模块移动到属于它自己的文件 src/front_of_house.rs 中:
pub mod hosting { pub fn add_to_waitlist() {} }
接下来就可以简化 crate根文件 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(); }
在 mod front_of_house 后使用 分号 ,而不是代码块,这将告诉 Rust 在另一个与 模块同名的文件 中 加载 模块的内容
在这个例子中,crate 根文件是 src/lib.rs,这也同样适用于以 src/main.rs 为 crate 根文件的二进制 crate 项
继续重构,将 hosting 模块也提取到其自己的文件中,仅对 src/front_of_house.rs 包含 hosting 模块的声明进行修改:
pub mod hosting;
接着创建一个 src/front_of_house 目录和一个包含 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 的一部分而编译时,use 不会有任何影响
- mod 关键字 声明 了 模块 ,Rust 会在 与模块同名的文件 中 查找 模块的代码
总结
- Rust 提供了:
- 将包组织进 crate
- 将 crate 组织进模块
- 指定路径从一个模块引用另一个模块中定义的项
- 通过使用 use 语句将路径引入作用域,在多次使用时可以使用更短的路径
- 模块定义的代码默认是私有的,不过可以选择增加 pub 关键字使其定义变为公有
接下来,来看看一些标准库提供的集合数据类型,可以利用它们编写出漂亮整洁的代码