Rust编程语言:使用package,crate和module来管理成长中的项目
- 1: 模块系统
- 2: Package和Crate
- 3: 定义模块以控制范围和隐私
- 4: 用于引用模块树中项目的路径
- 5: 用use关键字将路径纳入范围
- 6: 将模块分离到不同的文件中
1 - 模块系统
编写大型程序时,组织代码将是很重要的,因为在头脑中保持对整个程序的跟踪将变得不可能。通过对相关功能进行分组,并将具有不同特征的代码分开,你将明确在哪里可以找到实现某个特定功能的代码,以及在哪里可以改变某个功能的工作方式。
到目前为止,我们所写的程序都是在单个文件的单个模块中。随着项目的发展,可以通过把代码分成多个模块,然后再分成多个文件来组织代码。一个 package 可以包含多个二进制 crate,也可以选择一个库 crate。随着 package 的增长,可以将部分内容提取到独立的 crate 中,成为外部依赖。本章涵盖了所有这些技术。对于由一组相互关联的 package 组成的、共同发展的大型项目,Cargo提供了工作空间,我们将在第14章的 “Cargo工作空间” 部分介绍。
除了对功能进行分组外,封装实现细节还可以让你在更高层次上重用代码:一旦你实现了某个操作,其他代码就可以通过代码的 public 接口调用该代码,而不需要知道实现是如何工作的。你写代码的方式定义了哪些部分是 public 的,供其他代码使用,哪些部分是 private 的实现细节,你保留改变的权利。这是另一种限制你必须记在脑子里的细节数量的方法。
scope
是一个相关的概念:编写代码的嵌套上下文有一组被定义为 “in scope” 的名称。在阅读、编写和编译代码时,程序员和编译器需要知道某个特定位置的特定名称是否指的是一个变量、函数、结构体、枚举、模块、常量或其他项目,以及该项目意味着什么。你可以创建作用域并改变哪些名字在作用域内或作用域外。你不能在同一个作用域中有两个同名的项目;有工具可以解决名称冲突。
Rust有很多功能可以让你管理代码组织,包括哪些细节是 public 的,哪些细节是 private 的,以及在程序中每个范围内有哪些名字。这些功能,有时被统称为模块系统,包括:
-
Package: Cargo 的一个功能,可以建立、测试和分享 crate。
-
Crate:一个模块树,产生一个库或可执行文件
-
Module 和 use: 让你控制路径的组织、范围和隐私
-
Path:命名项目的方式,如结构体、函数或模块
在本章中,我们将介绍所有这些功能,讨论它们如何相互作用,并解释如何使用它们来管理范围。到最后,你应该对模块系统有一个扎实的了解,并且能够像专家一样使用作用域。
2 - Package和Crate
https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html
我们要讨论的模块系统的第一部分是 package 和 crate。crate 是一个二进制文件或库。crate root 是一个源文件,Rust编译器从它开始,构成了你的crate的根模块(我们将在 “定义模块以控制范围和隐私” 部分深入解释模块)。包是一个或多个提供一系列功能的crate。Package 包含一个 Cargo.toml
文件,描述如何构建这些 crate。
一些规则决定了 package 可以包含什么。一个 package 最多可以包含一个库crate。它可以包含任何你想要的二进制crate,但它必须至少包含一个crate(无论是库还是二进制)。
让我们来看看我们创建一个 package 时发生了什么。首先,我们输入 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
文件,给了我们一个 package。看一下 Cargo.toml
的内容,没有提到 src/main.rs
,因为 Cargo 遵循的惯例是 src/main.rs
是与 package 同名的二进制 crate 的 crate 根。同样地,Cargo 知道如果包的目录中包含 src/lib.rs
,那么该 package 就包含一个与该 package 同名的库 crate,而 src/lib.rs
是其 crate 根。Cargo 会将 crate 根文件传递给 rustc 来构建库或二进制文件。
这里,我们有一个只包含 src/main.rs
的 package,意味着它只包含一个名为 my-project
的二进制 crate。如果一个包包含 src/main.rs
和 src/lib.rs
,那么它就有两个crate:一个库和一个二进制,两者的名字都与包相同。通过在 src/bin
目录中放置文件,一个包可以有多个二进制 crate:每个文件都是一个单独的二进制 crate。
一个 crate 将把相关的功能集中在一个范围内,这样功能就很容易在多个项目之间共享。例如,我们在第二章中使用的 rand
crate 提供了生成随机数的功能。我们可以在自己的项目中使用该功能,方法是将 rand
crate 带入我们项目的作用域。所有由 rand
crate 提供的功能都可以通过 crate 的名称 rand
来访问。
将 crate 的功能保留在自己的范围内,可以明确特定的功能是在我们的 crate 还是rand crate 中定义的,并防止潜在的冲突。例如,rand
crate 提供了一个名为 Rng
的 trait。我们也可以在自己的 crate 中定义一个名为 Rng
的结构。因为 crate 的功能是在它自己的范围内命名的,所以当我们添加 rand
作为依赖关系时,编译器不会对 Rng
这个名字的含义感到困惑。在我们的 crate 中,它指的是我们定义的 Rng
结构体。我们将从 rand
crate 中访问 Rng
的特性,即 rand::Rng
。
让我们继续谈一谈模块系统吧。
3 - 定义模块以控制范围和隐私
https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html
在这一节中,我们将讨论模块和模块系统的其他部分,即允许你命名项目的路径;将路径带入范围的 use
关键字;以及使项目 public 的 pub
关键字。我们还将讨论 as
关键字、外部包和 glob
操作符。现在,让我们把重点放在模块上吧!
模块让我们把 crate 中的代码组织成一组,以提高可读性并便于重复使用。模块还可以控制项目的私密性,也就是一个项目是可以被外部代码使用(public)还是属于内部实现的细节(private),不能被外部使用。
作为例子,让我们写一个提供餐厅功能的crate。我们将定义函数的签名,但将其主体留空,以集中精力组织代码,而不是在代码中实际实现一个餐厅。
在餐饮业中,餐厅的某些部分被称为前厅,其他部分被称为后厅。前厅是顾客所在的地方;这里是主人为顾客安排座位,服务员接受订单和付款,调酒师调制饮料的地方。后厨是厨师在厨房工作的地方,洗碗工负责清理,经理负责行政工作。
为了按照真正的餐厅的工作方式来组织我们的 crate,我们可以将这些功能组织成嵌套模块。通过运行 cargo new --lib restaurant
创建一个名为 restaurant
的新库;然后将清单 7-1 中的代码放入 src/lib.rs
,以定义一些模块和函数签名。
文件名:src/lib.rs
:
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() {}
}
}
清单 7-1: 包含其他模块的 front_of_house
模块,这些模块又包含函数。
我们以 mod
关键字开始定义模块,然后指定模块的名称(本例中为 front_of_house
),并在模块的主体周围加上大括号。在模块内部,我们可以有其他的模块,如本例中的 hosting
和 serving
模块。模块还可以容纳其他项目的定义,如结构体、枚举、常量、特征,或如清单7-1中的函数。
通过使用模块,我们可以将相关的定义组合在一起,并说明它们为什么相关。使用这段代码的程序员会更容易找到他们想要使用的定义,因为他们可以根据分组来浏览代码,而不必阅读所有的定义。为这段代码添加新功能的程序员会知道该把代码放在哪里,以保持程序的条理性。
早些时候,我们提到 src/main.rs
和 src/lib.rs
被称为 crate roots
。之所以叫这个名字,是因为这两个文件中的任何一个文件的内容都构成了一个名为 crate
的模块,位于 crate
模块结构的根部,也就是所谓的模块树(module tree)。
清单 7-2 显示了清单 7-1 中结构的模块树:
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
清单7-2:清单7-1中代码的模块树
这棵树显示了一些模块是如何相互嵌套的(例如,hosting
嵌套在 front_of_house
里面)。这棵树还显示一些模块是彼此的兄弟姐妹,这意味着它们被定义在同一个模块中(hosting
和 serving
被定义在 front_of_house
中)。为了继续这个家庭隐喻,如果模块A包含在模块B里面,我们就说模块A是模块B的孩子,模块B是模块A的父母。请注意,整个模块树的根在名为crate的隐式模块下。
模块树可能会让你想起你电脑上的文件系统的目录树;这是一个非常恰当的比较!就像文件系统的目录一样。就像文件系统中的目录,你用模块来组织你的代码。就像目录中的文件一样,我们需要一种方法来找到我们的模块。
4 - 用于引用模块树中项目的路径
https://doc.rust-lang.org/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html
为了向 rust 展示在模块树中的哪里找到项目,我们使用路径,就像我们在浏览文件系统时使用路径一样。如果我们想调用一个函数,我们需要知道它的路径。
路径可以有两种形式:
-
绝对路径从 crate root 开始,通过使用 crate name 或字面 crate。
-
相对路径从当前模块开始,使用
self
、super
或当前模块中的标识符。
绝对路径和相对路径后面都有一个或多个标识符,用双冒号(::
)分开。
让我们回到清单7-1中的例子。我们如何调用 add_to_waitlist
函数?这就等于问,add_to_waitlist
函数的路径是什么?在清单7-3中,我们通过删除一些模块和函数来简化我们的代码。我们将展示两种方法,从定义在 crate root 的新函数 eat_at_restaurant
中调用 add_to_waitlist
函数。eat_at_restaurant
函数是我们库中 public API的一部分,所以我们用 pub
关键字标记它。在 “用pub关键字暴露路径” 一节中,我们将详细介绍 pub
。注意,这个例子还不能编译;我们稍后会解释原因。
文件名:src/lib.rs
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();
}
清单7-3:使用绝对和相对路径调用 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 root
开始,就像在 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,看看为什么它还不能编译! 我们得到的错误显示在清单7-4中:
$ 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 {
| ^^^^^^^^^^^
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant`
To learn more, run the command again with --verbose.
清单7-4:构建清单7-3中代码的编译器错误
这些错误信息说,模块托管是 private的。换句话说,我们有托管模块和 add_to_waitlist
函数的正确路径,但Rust不会让我们使用它们,因为它不能访问私有部分。
模块不仅对组织你的代码有用。它们还定义了Rust的隐私边界:封装了外部代码不允许知道、调用或依赖的实现细节的那一行。所以,如果你想让一个项目,如函数或结构变得私有,你就把它放在模块里。
Rust中 private 的工作方式是,所有项目(函数、方法、结构体、枚举、模块和常量)默认为私有。父模块中的项不能使用子模块中的私有项,但子模块中的项可以使用其祖先模块中的项。原因是,子模块包裹并隐藏了它们的实现细节,但子模块可以看到它们被定义的上下文。继续用餐厅的比喻,把隐私规则想成是餐厅的后台办公室:里面发生的事情对餐厅的顾客来说是私密的,但是办公室经理可以看到他们经营的餐厅里的一切,并进行操作。
Rust选择让模块系统这样运作,这样隐藏内部的执行细节是默认的。这样,你就知道你可以在不破坏外部代码的情况下改变内部代码的哪些部分。但是你可以通过使用 pub
关键字将一个项目公开,从而将子模块的内部代码暴露给外部祖先模块。
用pub关键字暴露路径
让我们回到清单 7-4 中的错误,告诉我们 hosting
模块是私有的。我们希望父模块中的 eat_at_restaurant
函数能够访问子模块中的 add_to_waitlist
函数,因此我们用 pub
关键字标记 hosting
模块,如清单 7-5 所示。
文件名:src/lib.rs
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();
}
不幸的是,清单7-5中的代码仍然导致了一个错误,如清单7-6所示。
$ 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();
| ^^^^^^^^^^^^^^^ 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:12:30
|
12 | 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: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant`
To learn more, run the command again with --verbose.
清单7-6:构建清单7-5中代码的编译器错误
发生了什么?在 mod hosting 前面添加 pub
关键字,使该模块成为公共模块。有了这个变化,如果我们可以访问 front_of_house
,我们就可以访问 hosting。但是 hosting 的内容仍然是私有的;使模块公开并没有使其内容公开。模块上的 pub
关键字只能让它的祖先模块的代码引用它。
清单 7-6 中的错误说 add_to_waitlist
函数是私有的。隐私规则适用于结构体、枚举、函数和方法以及模块。
让我们也通过在其定义前添加 pub
关键字使 add_to_waitlist
函数成为 public 函数,如清单7-7所示。
文件名:src/lib.rs
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();
}
清单7-7:在 mod hosting
和 fn add_to_waitlist
中添加 pub
关键字,让我们从 eat_at_restaurant
中调用该函数。
现在,代码将被编译! 让我们看看绝对路径和相对路径,并仔细检查一下为什么添加 pub 关键字可以让我们在 add_to_waitlist
中使用这些路径,以尊重隐私规则。
在绝对路径中,我们从 crate 开始,它是我们 crate 模块树的根。然后 front_of_house
模块被定义在 crate root 中。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 root 开始,而是从 front_of_house
开始。front_of_house
模块与eat_at_restaurant
定义在同一个模块中,所以从定义 eat_at_restaurant
的模块开始的相对路径起作用。然后,因为 hosting
和 add_to_waitlist
都被标记为pub,其余的路径都可以工作,这个函数调用是有效的!
用super开始相对路径
我们还可以通过在路径的开头使用 super 来构建从父模块开始的相对路径。这就像用 ..
语法来启动文件系统路径。为什么我们要这样做呢?
考虑一下清单7-8中的代码,它模拟了厨师修正错误的订单并亲自把它带给顾客的情况。函数 fix_incorrect_order
通过指定以 super
开头的 serve_order
的路径来调用函数 serve_order
。
文件名: src/lib.rs
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}
fn cook_order() {}
}
清单 7-8: 使用以super开头的相对路径调用函数
fix_incorrect_order
函数在 back_of_house
模块中,所以我们可以使用 super
转到 back_of_house
的父模块,在本例中是 crate,即根。从那里,我们寻找 serve_order
并找到它。成功了! 我们认为 back_of_house
模块和 serve_order
函数可能会保持相同的关系,如果我们决定重新组织crate的模块树,它们会被一起移动。因此,我们使用了super,这样,如果这段代码被移到不同的模块中,我们将有更少的地方需要更新代码。
将结构体和枚举公开
我们也可以使用 pub 来指定结构体和枚举为公共的,但有一些额外的细节。如果我们在结构体定义前使用pub,我们会使结构体成为公共的,但结构体的字段仍然是私有的。我们可以根据具体情况使每个字段公开或不公开。在清单 7-9 中,我们定义了一个 public 的 back_of_house::Breakfast
结构体,其中有一个 public 的烤面包字段,但有一个私有的 seasonal_fruit
字段。这模拟了餐厅的情况,即顾客可以选择随餐的面包类型,但是厨师会根据当季的水果和库存来决定随餐的水果。可用的水果变化很快,所以顾客无法选择水果,甚至无法看到他们会得到哪些水果。
文件名: src/lib.rs
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
中设置私有的季节性水果字段的值。
相反,如果我们把一个枚举变成公共的,那么它的所有变体都是公共的。我们只需要在 enum
关键字前加上pub,如清单7-10所示。
文件名: src/lib.rs
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。
5 - 用use关键字将路径纳入范围
https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html
到目前为止,我们所写的调用函数的路径可能看起来很不方便,而且是重复的。例如,在清单7-7中,无论我们选择绝对路径还是相对路径来调用add_to_waitlist
函数,每次我们想调用 add_to_waitlist
时都必须指定 front_of_house
和 hosting
。幸运的是,有一种方法可以简化这个过程。我们可以把一个路径带入一个作用域中,然后用 use
关键字来调用这个路径中的项目,就像它们是本地项目一样。
在清单 7-11 中,我们将 crate::front_of_house::hosting
模块带入 eat_at_restaurant
函数的作用域中,因此我们只需要指定 hosting::add_to_waitlist
来调用 eat_at_restaurant
的 add_to_waitlist
函数。
文件名: src/lib.rs
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();
}
清单 7-11: 用 use 将模块带入作用域
在作用域中添加 use
和路径类似于在文件系统中创建符号链接。通过在 crate root 中添加 use crate::front_of_house::hosting
,hosting
现在是该作用域中的一个有效名称,就像 hosting
模块在 crate
根中被定义一样。用 use 带入作用域的路径也检查隐私,就像其他路径一样。
你也可以用 use 和一个相对路径把一个项目带入作用域。清单 7-12 显示了如何指定一个相对路径以获得与清单 7-11 中相同的行为。
文件名: src/lib.rs
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();
}
清单 7-12: 用use和相对路径将模块带入作用域
创建习惯性的使用路径
在清单 7-11 中,你可能想知道为什么我们指定了 use crate::front_of_house::hosting
,然后在 eat_at_restaurant
中调用 hosting::add_to_waitlist
,而不是像清单 7-13 中那样一直指定 use
路径到 add_to_waitlist
函数以达到相同的结果。
文件名: src/lib.rs
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();
}
清单 7-13: 用 use 把 add_to_waitlist
函数带入作用域,这是不规范的做法
尽管清单 7-11 和 7-13 完成了同样的任务,但清单 7-11 是用 use 将函数带入作用域的习惯性方法。用 use 将函数的父模块带入作用域意味着我们必须在调用函数时指定父模块。在调用函数时指定父模块可以清楚地表明该函数不是本地定义的,同时还可以尽量减少全路径的重复。清单7-13中的代码不清楚add_to_waitlist
是在哪里定义的。
另一方面,当引入结构体、枚举和其他使用的项目时,指定完整的路径是一种习惯做法。清单 7-14 显示了将标准库的 HashMap 结构带入二进制 crate 的范围的习惯性方法。
文件名:src/main.rs
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
清单7-14:以一种习惯性的方式将HashMap带入范围内
这个习惯性背后没有什么强大的理由:这只是已经出现的惯例,人们已经习惯了这样阅读和编写Rust代码。
这个习惯的例外是,如果我们用use语句将两个同名的项目带入作用域,因为Rust不允许这样做。清单7-15显示了如何将两个名字相同但父模块不同的结果类型带入作用域以及如何引用它们。
文件名:src/lib.rs
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
Ok(())
}
清单 7-15: 将两个同名的类型带入同一个作用域需要使用它们的父模块。
正如你所看到的,使用父模块可以区分这两种结果类型。如果我们指定使用 std::fmt::Result
和使用 std::io::Result
,我们就会有两个Result类型在同一个作用域中,Rust就不知道我们使用Result时指的是哪一个。
用 as 关键字提供新的名字
对于用 use 将两个同名的类型带入同一个作用域的问题,还有一个解决办法:在路径之后,我们可以指定 as
和一个新的本地名称,或别名,作为该类型的名称。清单 7-16 显示了另一种编写清单 7-15 中代码的方法,即用 as
重命名两个 Result 类型中的一个。
文件名:src/lib.rs
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
在第二个使用语句中,我们为 std::io::Result
类型选择了新的名字 IoResult
,这不会与我们也带入范围的 std::fmt
的 Result 冲突。清单 7-15 和清单 7-16 被认为是习惯性的,所以选择由你决定
用pub的方式重新输出名字
当我们用 use
关键字将一个名字带入作用域时,新作用域中的名字是私有的。为了使调用我们代码的代码能够引用该名称,就像它被定义在该代码的作用域中一样,我们可以结合 pub
和 use
。这种技术被称为"再输出",因为我们把一个项目带入作用域,同时也使这个项目可以被其他人带入他们的作用域。
清单 7-17 显示了清单 7-11 中的代码,根模块中的 use
改为 pub use
。
文件名:src/lib.rs
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();
}
清单 7-17: 用pub use使名字可供任何代码从新的作用域中使用
通过使用 pub use
,外部代码现在可以使用 hosting::add_to_waitlist
调用 add_to_waitlist
函数。如果我们没有指定 pub 使用,eat_at_restaurant
函数可以在它的作用域中调用 hosting::add_to_waitlist
,但是外部代码不能利用这个新路径。
当你的代码的内部结构与调用你的代码的程序员对该领域的思考方式不同时,重新输出是很有用的。例如,在这个餐厅的比喻中,经营餐厅的人思考的是 “前厅 “和 “后厨”。但是来餐厅的顾客可能不会用这些术语来思考餐厅的各个部分。通过酒馆的使用,我们可以用一种结构来写我们的代码,但暴露出不同的结构。这样做使我们的库对从事库工作的程序员和调用库的程序员都有很好的组织。
使用外部包
在第二章中,我们编写了一个猜谜游戏项目,该项目使用了一个名为 rand
的外部包来获取随机数。为了在我们的项目中使用 rand
,我们在 Cargo.toml
中添加了这一行。
文件名:Cargo.toml
rand = "0.8.3"
在 Cargo.toml
中把 rand
作为一个依赖项,告诉 Cargo
从 crates.io
下载 rand
包和任何依赖项,使 rand
对我们的项目可用。
然后,为了将 rand
的定义带入我们包的范围,我们添加了一个以 crates.io
的名字为开头的 use
行,并列出了我们想带入范围的项目。回想一下,在第2章的"生成随机数"部分,我们将 Rng
特性带入范围,并调用了 rand::thread_rng
函数。
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..101);
}
Rust社区的成员在 crates.io
上提供了许多包,将它们中的任何一个拉到你的包中都包括这些相同的步骤:在你的包的 Cargo.toml
文件中列出它们,并使用 use
将它们的包中的项目引入范围。
请注意,标准库(std)也是我们包外部的一个crate。因为标准库是和Rust语言一起提供的,所以我们不需要修改 Cargo.toml
来包括std。但我们需要用use
来引用它,把那里的项目引入我们包的范围。例如,对于 HashMap
,我们可以使用这一行。
use std::collections::HashMap;
这是一个以 std 开头的绝对路径,是标准库 crate 的名称。
使用嵌套路径来清理大型使用列表
如果我们使用定义在同一crate或同一模块中的多个项目,将每个项目列在自己的行中会占用我们文件中大量的垂直空间。例如,我们在清单2-4中的猜谜游戏中的这两条使用语句将std中的项目带入范围。
文件名: src/main.rs
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--
相反,我们可以使用嵌套的路径,在一行中把相同的项目纳入范围。我们通过指定路径的共同部分,后面是两个冒号,然后用大括号列出路径中不同的部分,如清单7-18所示。
文件名: src/main.rs
// --snip--
use std::{cmp::Ordering, io};
// --snip--
清单 7-18: 指定嵌套路径,将具有相同前缀的多个项目带入作用域
在更大的程序中,使用嵌套路径将许多项目从同一个crate或模块带入作用域,可以减少很多单独的 use
语句的数量。
我们可以在路径中的任何一级使用嵌套路径,这在合并两个共享子路径的使用语句时很有用。例如,清单 7-19 显示了两条使用语句:一条将 std::io
带入作用域,另一条将 std::io::Write
带入作用域。
文件名:src/lib.rs
use std::io;
use std::io::Write;
清单7-19: 两个使用语句,其中一个是另一个的子路径
这两个路径的共同部分是 std::io
,这就是完整的第一个路径。为了将这两条路径合并为一条使用语句,我们可以在嵌套路径中使用 self
,如清单7-20所示。
文件名:src/lib.rs
use std::io::{self, Write};
清单7-20: 将清单 7-19 中的路径组合成一个 use 语句
这一行将 std::io
和 std::io::Write
带入范围。
Glob 操作符
如果我们想把某个路径中定义的所有公共项目都带入作用域,我们可以在该路径后面指定*
,即glob操作符。
use std::collections::*;
这个 use
语句将所有定义在 std::collection
中的公共项目带入当前的作用域。在使用 glob
操作符的时候要小心! glob
会使你更难分辨哪些名字在作用域中,以及你程序中使用的名字是在哪里定义的。
在测试时,glob
操作符经常被用来将所有被测试的东西带入测试模块;我们将在第11章的 “如何编写测试” 一节中讨论这个问题。glob 操作符有时也作为 prelude 模式的一部分使用:关于这种模式的更多信息,请参见标准库文档。
6 - 将模块分离到不同的文件中
https://doc.rust-lang.org/book/ch07-05-separating-modules-into-different-files.html
到目前为止,本章中所有的例子都是在一个文件中定义多个模块。当模块变得很大时,你可能想把它们的定义移到一个单独的文件中,以使代码更容易浏览。
例如,让我们从清单7-17中的代码开始,将 front_of_house
模块移到它自己的文件 src/front_of_house.rs
中,改变 crate root 文件,使其包含清单7-21中的代码。在这个例子中,crate root 文件是 src/lib.rs
,但这个过程也适用于 crate root 文件为 src/main.rs
的二进制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();
}
清单 7-21: 声明 front_of_house
模块,其主体将在 src/front_of_house.rs
中。
而 src/front_of_house.rs
则从 front_of_house
模块的主体中获取定义,如清单 7-22 所示。
文件名:src/front_of_house.rs
pub mod hosting {
pub fn add_to_waitlist() {}
}
清单 7-22: src/front_of_house.rs
中 front_of_house
模块内部的定义
在 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
的一部分被编译产生任何影响。mod 关键字声明了模块,Rust 会在与模块同名的文件中寻找进入该模块的代码。
总结
Rust允许你把包分成多个crate,把crate 分成模块,这样你就可以从另一个模块引用一个模块中定义的项目。你可以通过指定绝对或相对路径来做到这一点。这些路径可以被带入使用语句的范围内,这样你就可以在该范围内使用一个较短的路径来多次使用该项目。模块代码默认是私有的,但你可以通过添加pub关键字使定义公开。