使用 use
关键字将路径引入作用域
必须写出调用函数的路径可能会感觉不方便和重复。在清单 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
函数。
在作用域中添加 use
和路径类似于在文件系统中创建符号链接。通过在 crate 根目录中添加 use crate::front_of_house::hosting
,hosting
现在是该作用域中的有效名称,就像 hosting
模块在 crate 根目录中定义的一样。use
引入作用域的路径也会检查隐私性,就像其他任何路径一样。
请注意,use
仅为其发生的作用域创建快捷方式。列表 7-12 将 eat_at_restaurant
函数移动到名为 customer
的新子模块中,这与 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
,而不是将 use
路径一直指定到 add_to_waitlist
函数以达到相同的效果,如清单 7-13 所示。
虽然列表 7-11 和列表 7-13 实现了相同的功能,但列表 7-11 是使用 use
将函数引入作用域的惯用方法。通过 use
将函数的父模块引入作用域意味着在调用函数时必须指定父模块。在调用函数时指定父模块可以清楚地表明该函数不是本地定义的,同时仍然最小化了完整路径的重复。列表 7-13 中的代码不清楚 add_to_waitlist
是在哪里定义的。
另一方面,在使用 use
引入结构体、枚举和其他项时,
习惯上会指定完整路径。列表 7-14 展示了将标准库中的 HashMap
结构体引入二进制 crate 作用域的习惯用法。
这个惯用法背后没有强烈的原因:这只是已经形成的惯例,人们已经习惯以这种方式阅读和编写 Rust 代码。
这种惯用法的例外情况是,如果我们要通过 use
语句将两个同名的项引入作用域,因为 Rust 不允许这样做。列表 7-15 展示了如何将两个具有相同名称但父模块不同的 Result
类型引入作用域,以及如何引用它们。
正如您所见,使用父模块区分了两种 Result
类型。
如果我们指定 use std::fmt::Result
和 use std::io::Result
,我们会在同一作用域内有两个 Result
类型,Rust 将不知道我们使用 Result
时指的是哪一个。
使用 as
关键字提供新名称
还有另一种解决方案,可以使用 use
将同名的两种类型引入同一作用域:在路径之后,我们可以指定 as
和一个新的本地名称,或 别名,用于该类型。列表 7-16 显示了通过使用 as
重命名两个 Result
类型之一来编写列表 7-15 中代码的另一种方式。
在第二个 use
语句中,我们为 std::io::Result
类型选择了新的名称 IoResult
,这样就不会与我们也引入作用域的 std::fmt
中的 Result
冲突。清单 7-15 和清单 7-16 被认为是惯用的,所以选择权在你!
使用 pub use
重新导出名称
当我们使用 use
关键字将一个名称引入作用域时,该名称在新作用域中是私有的。为了使调用我们代码的代码能够像该名称在其作用域中定义的一样引用该名称,我们可以结合使用 pub
和 use
。这种技术称为 重新导出,因为我们不仅将一个项引入作用域,还使该项可供其他人引入到他们的作用域中。
列表 7-17 显示了列表 7-11 中的代码,其中根模块中的 use
更改为 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
,我们可以用一种结构编写代码,但暴露另一种结构。这样做可以使我们的库对开发库的程序员和调用库的程序员都井井有条。我们将在第14章的“使用pub use
导出方便的公共API”部分中查看另一个pub use
的例子及其如何影响你的crate的文档。
使用外部包
在第 2 章中,我们编写了一个猜数字游戏项目,该项目使用了一个名为 rand
的外部包来获取随机数。为了在我们的项目中使用 rand
,我们在 Cargo.toml 中添加了这一行:
在 Cargo.toml 中添加 rand
作为依赖项告诉 Cargo 从 crates.io 下载 rand
包及其所有依赖项,并使 rand
可用于我们的项目。
然后,为了将 rand
的定义引入我们包的作用域,我们添加了一行以 crate 名称 rand
开头的 use
语句,并列出了我们想要引入作用域的项。回想在第 2 章的 “生成随机数” 部分,我们将 Rng
特性引入了作用域并调用了 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
将它们的包中的项引入作用域。
请注意,标准库 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
中的项引入作用域:
相反,我们可以使用嵌套路径在一行中将相同的项引入作用域。我们通过指定路径的公共部分,后跟两个冒号,然后是大括号内的路径不同部分的列表来实现这一点,如清单 7-18 所示。
在较大的程序中,使用嵌套路径从同一个 crate 或模块中引入多个项可以大大减少所需的单独 use
语句的数量!
我们可以在路径的任何级别使用嵌套路径,这在组合两个共享子路径的 use
语句时非常有用。例如,列表 7-19 显示了两个 use
语句:一个将 std::io
引入作用域,另一个将 std::io::Write
引入作用域。
这两个路径的共同部分是std::io
,这就是完整的第一个路径。为了将这两个路径合并到一个use
语句中,我们可以在嵌套路径中使用self
,如清单7-20所示。
这行将 std::io
和 std::io::Write
引入作用域。
通配符操作符
如果我们想将路径中定义的所有公共项引入作用域,我们可以指定该路径后跟*
通配符:
#![allow(unused)] fn main() { use std::collections::*; }
这个 use
语句将 std::collections
中定义的所有公共项引入当前作用域。使用通配符时要小心!通配符会使确定哪些名称在作用域内以及程序中使用的名称是在哪里定义的变得更加困难。
通配符操作符通常在测试时用于将所有待测试的内容引入到 tests
模块;我们将在第 11 章的 “如何编写测试” 部分讨论这个问题。通配符操作符有时也作为前言模式的一部分使用:请参阅 标准库文档 以获取有关该模式的更多信息。