使用 use 关键字将路径引入作用域

每次调用函数时都写出路径可能会让人感到不便和重复。在 Listing 7-7 中,无论我们选择的是绝对路径还是相对路径来调用 add_to_waitlist 函数,每次我们想要调用 add_to_waitlist 时,都必须指定 front_of_househosting。幸运的是,有一种方法可以简化这个过程:我们可以使用 use 关键字一次性创建一个路径的快捷方式,然后在作用域内的其他地方使用这个更短的名字。

在 Listing 7-11 中,我们将 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(); }

在作用域中添加 use 和路径类似于在文件系统中创建一个符号链接。通过在 crate 根目录中添加 use crate::front_of_house::hostinghosting 现在在该作用域中是一个有效的名字,就像 hosting 模块被定义在 crate 根目录中一样。使用 use 引入作用域的路径也会像其他路径一样检查私有性。

需要注意的是,use 只会在其出现的作用域中创建快捷方式。Listing 7-12 将 eat_at_restaurant 函数移动到一个名为 customer 的新子模块中,这个子模块的作用域与 use 语句的作用域不同,因此函数体将无法编译。

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(); } }

编译器错误显示,快捷方式在 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 路径

在 Listing 7-11 中,你可能会好奇为什么我们指定了 use crate::front_of_house::hosting,然后在 eat_at_restaurant 中调用 hosting::add_to_waitlist,而不是直接将 use 路径一直延伸到 add_to_waitlist 函数,以达到相同的结果,如 Listing 7-13 所示。

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(); }

尽管 Listing 7-11 和 Listing 7-13 完成了相同的任务,但 Listing 7-11 是使用 use 将函数引入作用域的惯用方式。使用 use 将函数的父模块引入作用域意味着我们在调用函数时必须指定父模块。在调用函数时指定父模块可以清楚地表明该函数不是在本地定义的,同时还能最大限度地减少完整路径的重复。Listing 7-13 中的代码不清楚 add_to_waitlist 是在哪里定义的。

另一方面,当使用 use 引入结构体、枚举和其他项时,惯用的做法是指定完整路径。Listing 7-14 展示了将标准库的 HashMap 结构体引入二进制 crate 作用域的惯用方式。

use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }

这种惯用方式背后没有特别的原因:这只是已经形成的惯例,人们已经习惯了以这种方式阅读和编写 Rust 代码。

这种惯用方式的例外情况是,如果我们使用 use 语句将两个同名的项引入作用域,因为 Rust 不允许这样做。Listing 7-15 展示了如何将两个同名但父模块不同的 Result 类型引入作用域,以及如何引用它们。

use std::fmt; use std::io; fn function1() -> fmt::Result { // --snip-- Ok(()) } fn function2() -> io::Result<()> { // --snip-- Ok(()) }

如你所见,使用父模块可以区分这两个 Result 类型。如果我们指定 use std::fmt::Resultuse std::io::Result,我们将在同一作用域中有两个 Result 类型,Rust 将不知道我们使用 Result 时指的是哪一个。

使用 as 关键字提供新名称

使用 use 将两个同名类型引入同一作用域的另一种解决方案是:在路径之后,我们可以指定 as 和一个新的本地名称,或称为 别名。Listing 7-16 展示了通过使用 as 重命名其中一个 Result 类型来编写 Listing 7-15 中的代码的另一种方式。

use std::fmt::Result; use std::io::Result as IoResult; fn function1() -> Result { // --snip-- Ok(()) } fn function2() -> IoResult<()> { // --snip-- Ok(()) }

在第二个 use 语句中,我们为 std::io::Result 类型选择了新名称 IoResult,这样它就不会与我们从 std::fmt 引入的 Result 冲突。Listing 7-15 和 Listing 7-16 都被认为是惯用的,所以选择权在你手中!

使用 pub use 重新导出名称

当我们使用 use 关键字将名称引入作用域时,新作用域中的名称是私有的。为了使调用我们代码的代码能够像在其作用域中定义的那样引用该名称,我们可以将 pubuse 结合起来。这种技术称为 重新导出,因为我们不仅将项引入作用域,还使其可供其他人引入他们的作用域。

Listing 7-17 展示了 Listing 7-11 中的代码,将根模块中的 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(); }

在此更改之前,外部代码必须使用路径 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,我们可以用一种结构编写代码,但暴露另一种结构。这样做可以使我们的库对编写库的程序员和调用库的程序员都组织良好。我们将在第 14 章的 “使用 pub use 导出方便的公共 API” 中看到另一个 pub use 的例子以及它如何影响你的 crate 文档。

使用外部包

在第 2 章中,我们编写了一个猜数字游戏项目,该项目使用了一个名为 rand 的外部包来获取随机数。要在我们的项目中使用 rand,我们在 Cargo.toml 中添加了这行代码:

rand = "0.8.5"

Cargo.toml 中添加 rand 作为依赖项会告诉 Cargo 从 crates.io 下载 rand 包及其所有依赖项,并使 rand 可用于我们的项目。

然后,为了将 rand 的定义引入我们的包的作用域,我们添加了一个以 crate 名称 rand 开头的 use 行,并列出了我们想要引入作用域的项。回想一下,在第 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 或同一模块中定义的多个项,将每个项单独列在一行可能会占用文件中的大量垂直空间。例如,我们在 Listing 2-4 的猜数字游戏中有这两个 use 语句,将 std 中的项引入作用域:

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!"), } }

相反,我们可以使用嵌套路径在一行中将相同的项引入作用域。我们通过指定路径的公共部分,后跟两个冒号,然后用花括号括起路径的不同部分来实现这一点,如 Listing 7-18 所示。

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!"), } }

在较大的程序中,使用嵌套路径从同一 crate 或模块中引入许多项可以大大减少所需的单独 use 语句的数量!

我们可以在路径的任何级别使用嵌套路径,这在组合两个共享子路径的 use 语句时非常有用。例如,Listing 7-19 展示了两个 use 语句:一个将 std::io 引入作用域,另一个将 std::io::Write 引入作用域。

use std::io; use std::io::Write;

这两个路径的公共部分是 std::io,这是第一个完整路径。要将这两个路径合并为一个 use 语句,我们可以在嵌套路径中使用 self,如 Listing 7-20 所示。

use std::io::{self, Write};

这行代码将 std::iostd::io::Write 引入作用域。

使用 Glob 运算符

如果我们想将路径中定义的所有公共项引入作用域,可以指定该路径后跟 * glob 运算符:

#![allow(unused)] fn main() { use std::collections::*; }

这个 use 语句将 std::collections 中定义的所有公共项引入当前作用域。使用 glob 运算符时要小心!Glob 可能会使你在程序中使用的名称的来源变得难以辨认。

glob 运算符通常用于测试中,将测试中的所有内容引入 tests 模块;我们将在第 11 章的 “如何编写测试” 中讨论这一点。glob 运算符有时也作为 prelude 模式的一部分使用:有关该模式的更多信息,请参阅 标准库文档