使用 use
关键字将路径引入作用域
为了调用函数而写出路径可能会让人觉得不方便和重复。在示例 7-7 中,无论我们选择 add_to_waitlist
函数的绝对路径还是相对路径,每次我们想要调用 add_to_waitlist
时,都必须同时指定 front_of_house
和 hosting
。幸运的是,有一种简化此过程的方法:我们可以使用 use
关键字为路径创建一个快捷方式,然后在作用域中的任何其他地方使用较短的名称。
在示例 7-11 中,我们将 crate::front_of_house::hosting
模块引入 eat_at_restaurant
函数的作用域,这样在 eat_at_restaurant
中调用 add_to_waitlist
函数时,我们只需要指定 hosting::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();
}
示例 7-11:使用 use
将模块引入作用域
在作用域中添加 use
和路径类似于在文件系统中创建符号链接。通过在 crate 根部添加 use crate::front_of_house::hosting
,hosting
现在在该作用域中是一个有效的名称,就像 hosting
模块在 crate 根部定义的一样。使用 use
引入作用域的路径也会像其他路径一样检查私有性。
请注意,use
仅为 use
出现的特定作用域创建快捷方式。示例 7-12 将 eat_at_restaurant
函数移动到一个名为 customer
的新子模块中,这与 use
语句的作用域不同,因此函数体将无法编译。
文件名:src/lib.rs
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 build
Compiling restaurant v0.1.0 (file:///projects/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 1 previous error; 1 warning emitted
请注意,还有一个警告,指出 use
在其作用域中不再使用!要解决此问题,请将 use
也移动到 customer
模块中,或者在子 customer
模块中使用 super::hosting
引用父模块中的快捷方式。
创建符合惯例的 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();
}
示例 7-13:使用 use
将 add_to_waitlist
函数引入作用域,这不符合惯例
虽然示例 7-11 和示例 7-13 都完成了相同的任务,但示例 7-11 是使用 use
将函数引入作用域的符合惯例的方式。使用 use
将函数的父模块引入作用域意味着我们在调用函数时必须指定父模块。在调用函数时指定父模块可以清楚地表明该函数不是本地定义的,同时最大限度地减少完整路径的重复。示例 7-13 中的代码不清楚 add_to_waitlist
在哪里定义。
另一方面,当使用 use
引入结构体、枚举和其他项时,符合惯例的做法是指定完整路径。示例 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 展示了如何将两个同名但父模块不同的 Result
类型引入作用域,以及如何引用它们。
文件名:src/lib.rs
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
Ok(())
}
示例 7-15:将两个同名类型引入同一作用域需要使用它们的父模块。
如您所见,使用父模块区分了这两个 Result
类型。相反,如果我们指定 use std::fmt::Result
和 use std::io::Result
,我们将在同一作用域中拥有两个 Result
类型,而 Rust 将不知道我们使用 Result
时指的是哪一个。
使用 as
关键字提供新名称
对于使用 use
将两个同名类型引入同一作用域的问题,还有另一种解决方案:在路径之后,我们可以为该类型指定 as
和一个新的本地名称,或别名。示例 7-16 展示了通过使用 as
重命名两个 Result
类型之一来编写示例 7-15 中代码的另一种方式。
文件名:src/lib.rs
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
示例 7-16:当使用 as
关键字将类型引入作用域时重命名类型
在第二个 use
语句中,我们为 std::io::Result
类型选择了新名称 IoResult
,这不会与我们也引入作用域的 std::fmt
中的 Result
冲突。示例 7-15 和示例 7-16 被认为是符合惯例的,因此选择权取决于您!
使用 pub use
重新导出名称
当我们使用 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();
}
示例 7-17:使用 pub use
使名称可供任何代码从新作用域中使用
在此更改之前,外部代码必须使用路径 restaurant::front_of_house::hosting::add_to_waitlist()
调用 add_to_waitlist
函数,这也将需要将 front_of_house
模块标记为 pub
。现在,由于此 pub use
已从根模块重新导出 hosting
模块,因此外部代码可以使用路径 restaurant::hosting::add_to_waitlist()
代替。
当您代码的内部结构与调用您代码的程序员对域的看法不同时,重新导出非常有用。例如,在这个餐厅的比喻中,经营餐厅的人会想到“前厅”和“后厨”。但是光顾餐厅的顾客可能不会以这些术语来考虑餐厅的各个部分。使用 pub use
,我们可以用一种结构编写代码,但公开另一种结构。这样做使我们的库对于开发库的程序员和调用库的程序员来说都组织良好。我们将在 “使用 pub use
导出便捷的公共 API” 中查看 pub use
的另一个示例以及它如何影响您的 crate 的文档。第 14 章的章节。
使用外部包
在第 2 章中,我们编写了一个猜数字游戏项目,该项目使用一个名为 rand
的外部包来获取随机数。为了在我们的项目中使用 rand
,我们在 Cargo.toml 中添加了这一行
文件名:Cargo.toml
rand = "0.8.5"
在 Cargo.toml 中将 rand
添加为依赖项会告诉 Cargo 从 crates.io 下载 rand
包及其任何依赖项,并使 rand
可用于我们的项目。
然后,为了将 rand
定义引入我们包的作用域,我们添加了一个 use
行,以 crate 的名称 rand
开头,并列出了我们想要引入作用域的项。回想一下在 “生成随机数”第 2 章的章节中,我们将 Rng
trait 引入作用域并调用了 rand::thread_rng
函数
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
Rust 社区的成员已经在 crates.io 上提供了许多包,将它们中的任何一个拉入您的包都涉及相同的步骤:在您包的 Cargo.toml 文件中列出它们,并使用 use
将项从它们的 crate 引入作用域。
请注意,标准 std
库也是一个外部于我们包的 crate。因为标准库与 Rust 语言一起发布,所以我们不需要更改 Cargo.toml 以包含 std
。但是我们需要使用 use
引用它,以便将那里的项引入我们包的作用域。例如,对于 HashMap
,我们将使用这行代码
#![allow(unused)] fn main() { use std::collections::HashMap; }
这是一个以 std
开头的绝对路径,std
是标准库 crate 的名称。
使用嵌套路径清理大型 use
列表
如果我们使用在同一 crate 或同一模块中定义的多个项,则将每个项单独列在一行上可能会占用文件中的大量垂直空间。例如,我们在示例 2-4 的猜数字游戏中使用的这两个 use
语句将 std
中的项引入作用域
文件名:src/main.rs
use rand::Rng;
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
相反,我们可以使用嵌套路径在一行中将相同的项引入作用域。我们通过指定路径的公共部分,后跟两个冒号,然后在花括号中包含路径中不同部分的列表来完成此操作,如示例 7-18 所示。
文件名:src/main.rs
use rand::Rng;
// --snip--
use std::{cmp::Ordering, io};
// --snip--
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
示例 7-18:指定嵌套路径以将具有相同前缀的多个项引入作用域
在较大的程序中,使用嵌套路径从同一 crate 或模块中引入许多项可以大大减少所需的单独 use
语句的数量!
我们可以在路径中的任何级别使用嵌套路径,这在组合共享子路径的两个 use
语句时非常有用。例如,示例 7-19 显示了两个 use
语句:一个将 std::io
引入作用域,另一个将 std::io::Write
引入作用域。
文件名:src/lib.rs
use std::io;
use std::io::Write;
示例 7-19:两个 use
语句,其中一个语句是另一个语句的子路径
这两个路径的公共部分是 std::io
,那是完整的第一个路径。为了将这两个路径合并为一个 use
语句,我们可以在嵌套路径中使用 self
,如示例 7-20 所示。
文件名:src/lib.rs
use std::io::{self, Write};
示例 7-20:将示例 7-19 中的路径合并为一个 use
语句
这一行将 std::io
和 std::io::Write
引入作用域。
Glob 运算符
如果我们想将路径中定义的所有公共项引入作用域,我们可以指定该路径,后跟 *
glob 运算符
#![allow(unused)] fn main() { use std::collections::*; }
此 use
语句将 std::collections
中定义的所有公共项引入当前作用域。使用 glob 运算符时要小心!Glob 可能会使您更难分辨哪些名称在作用域中以及程序中使用的名称在哪里定义。
glob 运算符通常在测试时使用,以将所有被测项引入 tests
模块;我们将在 “如何编写测试”第 11 章的章节中讨论这一点。glob 运算符有时也用作 prelude 模式的一部分:有关该模式的更多信息,请参见 标准库文档。有关该模式的更多信息。