简介
欢迎来到「使用 use 关键字将路径引入作用域」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,我们将学习如何使用 use 关键字将路径引入作用域,从而为调用函数和模块创建快捷方式。
欢迎来到「使用 use 关键字将路径引入作用域」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,我们将学习如何使用 use 关键字将路径引入作用域,从而为调用函数和模块创建快捷方式。
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 模块中,快捷方式不再适用:
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`
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
请注意,还有一个警告,表明 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--
}
fn function2() -> io::Result<()> {
--snip--
}
清单 7-15:将两个同名类型引入同一作用域时需要使用它们的父模块
如你所见,使用父模块可以区分这两个 Result 类型。如果我们改为指定 use std::fmt::Result 和 use std::io::Result,那么在同一作用域中就会有两个 Result 类型,当我们使用 Result 时,Rust 将不知道我们指的是哪一个。
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--
}
fn function2() -> IoResult<()> {
--snip--
}
清单 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 函数。现在,由于这个 pub use 从根模块重新导出了 hosting 模块,外部代码可以改为使用路径 restaurant::hosting::add_to_waitlist()。
当你的代码的内部结构与调用你代码的程序员对领域的思考方式不同时,重新导出会很有用。例如,在这个餐厅的比喻中,经营餐厅的人会考虑“前台”和“后台”。但是去餐厅就餐的顾客可能不会用这些术语来考虑餐厅的各个部分。通过 pub use,我们可以用一种结构编写代码,但暴露另一种结构。这样做能使我们的库对于编写库的程序员和调用库的程序员来说都组织良好。我们将在“使用 pub use 导出方便的公共 API”中查看 pub use 的另一个示例以及它如何影响你的 crate 的文档。
在第 2 章中,我们编写了一个猜数字游戏项目,该项目使用了一个名为 rand 的外部包来获取随机数。为了在我们的项目中使用 rand,我们在 Cargo.toml 中添加了这一行:
文件名:Cargo.toml
rand = "0.8.5"
在 Cargo.toml 中将 rand 添加为依赖项会告诉 Cargo 从 https://crates.io 下载 rand 包及其所有依赖项,并使 rand 对我们的项目可用。
然后,为了将 rand 的定义引入我们包的作用域,我们添加了一行以包名 rand 开头的 use 语句,并列出了我们想要引入作用域的项。回想一下在“生成随机数”中,我们将 Rng 特性引入作用域并调用了 rand::thread_rng 函数:
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..=100);
}
Rust 社区的成员在 https://crates.io 上提供了许多包,将其中任何一个包引入你的包都涉及相同的步骤:在你的包的 Cargo.toml 文件中列出它们,并使用 use 将来自它们包的项引入作用域。
请注意,标准的 std 库也是我们包外部的一个包。因为标准库是随 Rust 语言一起提供的,所以我们不需要更改 Cargo.toml 来包含 std。但是我们确实需要使用 use 来引用它,以便将那里的项引入我们包的作用域。例如,对于 HashMap,我们会使用这一行:
use std::collections::HashMap;
这是一个以标准库包名 std 开头的绝对路径。
use 列表如果我们要使用在同一个包或同一个模块中定义的多个项,在文件中每行单独列出每个项会占用大量垂直空间。例如,在清单 2-4 的猜数字游戏中,我们有这两条 use 语句将 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:指定嵌套路径以将具有相同前缀的多个项引入作用域
在更大的程序中,使用嵌套路径从同一个包或模块中引入许多项可以大大减少所需的单独 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 引入作用域。
如果我们想将在某个路径中定义的 所有 公共项引入作用域,可以指定该路径,后跟 * 通配符运算符:
use std::collections::*;
这条 use 语句将 std::collections 中定义的所有公共项引入当前作用域。使用通配符运算符时要小心!通配符可能会使判断哪些名称在作用域内以及程序中使用的名称是在哪里定义的变得更加困难。
通配符运算符在测试时经常用于将所有要测试的内容引入 tests 模块;我们将在“如何编写测试”中讨论这一点。通配符运算符有时也用作前奏模式的一部分:有关该模式的更多信息,请参阅标准库文档。
恭喜你!你已经完成了“使用 use 关键字将路径引入作用域”实验。你可以在 LabEx 中练习更多实验来提升你的技能。