包、crate 与 模块
编写程序时一个核心的问题是作用域:在代码的某处编译器知道哪些变量名?允许调用哪些函数?这些变量引用的又是什么?
Rust
有一系列与作用域相关的功能。这有时被称为模块系统,不过又不仅仅是模块:
包是Cargo的一个功能,它允许你构建、测试和分享crate。
Crates是一个模块的树形结构,它形成了库或二进制项目。
模块和
use
关键字允许你控制作用域和路径的私有性。路径是一个命名例如结构体、函数或模块等项的方式
包和 crate 用来创建库和二进制项目
让我们来聊聊模块和 crate。下面是一个总结:
- crate 是一个二进制或库项目。
- crate 根是一个用来藐视如何构建 crate 的文件。
- 带有 Cargo.toml 文件的包用以描述如何构建一个或多个 crate。一个包中至多有一个库项目。
所以当运行cargo new
时是在创建一个包:
cargo new my-project
因为 Cargo 创建了 Cargo.toml,这意味者现在我们有了一个包。如果查看 Cargo.toml 的内容,会发现并没有提到 src/main.rs。然而,Cargo 的约定是如果在代表表的 Cargo.toml 的同级目录下包含 src 目录且其中包含 main.rs 文件的话,Cargo 就知道这个包带有一个与包同名的二进制 crate,且 src/main.rs 就是 crate 根。另一个约定如果包目录中包含 src/lib.rs,则包带有与其同名的库 crate,且 src/lib.rs 是 crate 根。crate 根文件将由 Cargo 传递给 rustc
来实际构建库或者二进制项目。
一个包可以带有零个或一个库 crate 和任意多个二进制 crate。一个包中至少一个(库或二进制) crate。
模块系统来控制作用域和私有性
Rust 的此部分功能通常被引用为模块系统,不过其包括了一些除模块之外的功能。本部分我们会讨论:
- 模块,一个组织代码和控制路径私有性的方式
- 路径,一个命名项的方式
-
use
关键字用来将路径引入作用域 -
pub
关键字使项变为共有 -
as
关键字用于将项引入作用域时进行重命名 - 使用外部包
- 嵌套路径来消除大量的
use
语句 - 使用 glob 运算符将模块的所有内容引入作用域
- 如何将不同模块分割到单独的文件中
模块允许我们将代码组织起来。
mod sound {
mod instrument {
mod woodwind {
fn clarinet() {
}
}
}
mod voice {
}
}
fn main() {
}
路径来引用模块树中的项
如果想要调用函数,需要知道其路径。
路径有两种形式:
- 绝对路径从crate 根开始,以 crate 名或者字面值
crate
开头 - 相对路径从当前模块开始,以
self
、supper
或者当前模块的标识符开头。
绝对路径和相对路径都后跟一个或多个由双冒号(::
)分割的标识符。
在main
中调用clarinet
函数,现在还不能调用
mod sound {
mod instrument {
fn clarinet() {
}
}
}
fn main() {
// 绝对路径
crate::sound::instrument::clarinet();
// 相对路径
sound::instrument::clarinet();
}
instrument
模块是私有的。instrument
模块和clarinet
函数的路径都是正确的,不过Rust
不让我们使用,因为他们是私有的。
模块作为私有性的边界
Rust
采用模块还有另外一个原因:模块是Rust
中的私有边界。私有规则如下:
- 所有项(函数、方法、结构体、枚举、模块和常量)默认是私有的。
- 可以使用
pub
关键字使项变为共有。 - 不允许使用定义于当前模块的子模块中私有代码。
- 允许使用任何定义于父模块或当前模块中的代码
使用pub
关键字使项变为公有
mod sound {
pub mod instrument {
pub fn clarinet() {
}
}
}
fn main() {
// 绝对路径
crate::sound::instrument::clarinet();
// 相对路径
sound::instrument::clarinet();
}
使用super
开始相对路径
fn main() {
crate::sound::instrument::clarinet();
sound::instrument::inner::clarinet();
}
mod sound {
pub mod instrument {
pub fn clarinet() {
super::voice::hello();
}
pub mod inner {
pub fn clarinet() {
super::super::voice::hello();
}
}
}
pub mod voice {
pub fn hello() {
println!("hello");
}
}
}
对结构体和枚举使用pub
mod plant {
pub struct Vegetable {
pub name: String,
id: i32,
}
impl Vegetable {
pub fn new(name: &str) -> Vegetable {
Vegetable {
name: String::from(name),
id: 1,
}
}
}
}
fn main() {
let mut v = plant::Vegetable::new("squash");
v.name = String::from("butternut squash");
println!("{}", v.name);
}
plant::Vegetable
结构体的name
字段是公有的,在main
中可以使用点号读写name
字段。不允许在main
中使用id
字段因为是私有的。
使用use
关键字将名称引入作用域
mod sound {
pub mod instrument {
pub fn clarinet() {
}
}
}
use crate::sound::instrument;
fn main() {
instrument::clarinet();
}
使用相对路径将项引入作用域
use self::sound::instrument;
user
函数路径使用习惯 VS 其他项
mod sound {
pub mod instrument {
pub fn clarinet() {
}
}
}
use crate::sound::instrument::clarinet;
fn main() {
clarinet();
}
user
将clarinet
函数引入作用域,这是不推荐的。
对于函数来说,通过use
指定的父模块接着指定模块来调用方法被认为是习惯的方法。通过use
指定函数的路径,清楚的表明了函数不是本地定义的,同时最小化了指定全路径的重复。
通过 as
关键字重命名引入作用域的类型
将两个同名类型引入同一作用域这个问题还有另一个解决办法:可以通过在use
后加上as
和一个新名称来为此类型指定一个新的本地名称。
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
Ok(())
}
fn function2() -> IoResult<()> {
Ok(())
}
通过pub use
重导出名称
当使用 use
关键字将名称导入作用域时,在新作用域中可用的名称是私有的。如果希望调用呢编写的代码能够像你一样在其自己的作用域内引用这些类型,可以结合pub
和use
。这个技术被称为重导出,因为这样做将项引入作用域同时使其可供其他代码引入自己的作用域。
mod sound {
pub mod instrument {
pub fn clarinet() {
}
}
}
mod performance_group {
pub use crate::sound::instrument;
pub fn clarinet_trio() {
instrument::clarinet();
}
}
fn main() {
performance_group::clarinet_trio();
performance_group::instrument::clarinet();
}
使用外部包
在 Cargo.toml 中加入 rand
依赖
[dependencies]
rand = "0.5.5"
将rand
定义引入项目包的作用域,加入一行use
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1, 101);
}
嵌套路径来消除大量的use
行
当需要引入很多定义于相同或相同模块的项时,为每一项单独列出一行会占用源码很大的空间。
use std::cmp::Ordering;
use std::io;
// ...
可以使用嵌套的路径将同样的项在一行中引入而不是两行。
use std::{cmp::Ordering, io};
通过 glob 运算符将所有的公有定义引入作用域
如果希望将一个路径下 所有 公有项引入作用域,可以指定路径后跟*
,glob 运算符:
use std::collections::*;
将模块分割进不同文件
当模块变得更大时,你可能想要将它们的定义移动到一个单独的文件中使代码更容易阅读。
文件名: src/main.rs
mod sound;
fn main() {
// 绝对路径
create::sound::instrument::clarinet();
// 相对路径
sound::instrument::clarinet();
}
在crate根文件声明 sound
模块(这里是 src/main.rs),将模块内容移动到 src/sound.rs 文件, src/sound.rs 中会包含sound
模块的内容
文件名: src/sound.rs
pub mod instrument {
pub fn clarinet() {
}
}
在mod sound
后使用分号而不是代码块告诉Rust在另一个与模块同名文件中加载模块的内容。
继续重构我们的例子,将instrument
模块也提取到自己的文件中,修改 src/sound.rs 只包含instrument
模块的声明:
文件名: src/sound.rs
pub mod instrument;
接着创建 src/sound 目录和 src/sound/instrument.rs 文件来包含instrument
模块的定义:
文件名: src/sound/instrument.rs
pub fn clarinet() {
}
模块树依然保持相同,main
中的函数调用也无需修改继续保持有效,即使其定义存在于不同的文件中。这样随着代码增长可以将模块移动到新文件中。
总结
Rust 提供了将包组织进 crate, 将 crate 组织进模块和通过指定绝对或相对路径从一个模块引用另一个模块中定义的项的方式。可以通过use
语句将路径引入作用域,这样在多次使用时可以使用更短的路径。模块定义的代码默认是私有的,不过可以选择增加pub
关键字使其定义变为公有。