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

Bringing Paths into Scope with the use Keyword

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

在清单 7-11 中,我们将 crate::front_of_house::hosting 模组,引入了 eat_at_restaurant 函数的作用域,因此只需指明 hosting::add_too_waitlist,就能调用 eat_at_restaurant 中的 add_too_waitlist 函数。

文件名:src/lib.rs

#![allow(unused)]
fn main() {
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 关键字及某个路径,类似于在文件系统中创建一个符号链接。而通过在代码箱根中添加 use crate::front_of_house::hostinghosting 现在便是该作用域中的一个有效名字,就像在代码箱根中已经定义了 hosting 模组一样。使用 use 关键字纳入作用域的路径,也会像其他路径一样检查隐私。

请注意,use 关键字只会为该 use 语句所在的特定作用域,创建快捷方式。清单 7-12 将 eat_at_restaurant 函数,移到了一个名为 customer 的新子模组中,该子模组的作用域与 use 语句的作用域不同,因此这个函数体就无法编译。

文件名:src/lib.rs

#![allow(unused)]
fn main() {
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 (/home/peng/rust-lang/restaurant)
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
  --> src/lib.rs:33:9
   |
33 |         hosting::add_to_waitlist();
   |         ^^^^^^^ use of undeclared crate or module `hosting`

warning: unused import: `crate::front_of_house::hosting`
  --> src/lib.rs:28:5
   |
28 | 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` due to previous error; 1 warning emitted

请注意这里还有个警告,即在其作用域中那个 use 不再被用到!要解决这个问题,同时要把这个 use 语句,移到 customer 模组中,或者在 customer 子模组中,使用 super::hosting 引用父模组中的那个快捷方式。

创建惯用 use 路径

Creating Idiomatic use Paths

清单 7-11 中,咱们可能想知道,为什么我们指定了使用 crate::front_of_house::hosting,然后在 eat_at_restaurant 中调用 hosting::add_to_waitlist,而不是像清单 7-13 中那样,为了达到同样的效果,指定出直达 add_too_waitlist 函数的 use 路径。

文件名:src/lib.rs

#![allow(unused)]
fn main() {
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:使用 useadd_to_waitlist 纳入作用域,这种非惯常用法

尽管清单 7-11 和清单 7-13 都完成了同样任务,但清单 7-11 是将函数纳入作用域的惯用方法。使用 use 将函数的父模组纳入作用域,意味着我们必须在调用该函数时,指明其父模组。在调用函数时指定父模组,可以清楚地表明该函数不是本地定义的,同时仍然最大限度减少了完整路径的重复。清单 7-13 中的代码并不明晰,因为add_too_waitlist 的定义位置不明晰。

而另一方面,在以 use 关键字引入结构体、枚举及其他一些项目时,指定完整路径就是种习惯做法。清单 7-14 展示了将标准库的 HashMap 结构体,纳入某个二进制代码箱作用域的惯用方法。

文件名: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

#![allow(unused)]
fn main() {
use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --跳过--
}

fn function2() -> io::Result {
    // --跳过--
}
}

清单 7-15:将两个同名类型纳入同一作用域,就需要用到他们的父模组

正如咱们所见到的,使用父模组就可以区分这两种 Result 类型。而如果我们指定了 use std::fmt::Resultuse std::io::Result,我们就会在同一作用域中,有了两个 Result 类型,但 Rust 不会知道,我们使用 Result 时指的是哪个。

使用 as 关键字提供新名字

Providing New Names with the as Keyword

还有另一种方法,可以解决将两个同名类型纳入同一作用域的问题:在路径之后,我们可以指定 as 和一个新的本地名字,或 别名,alias。清单 7-16 展示了另一种编写清单 7-15 代码的方法,即使用 as 重命名两个 Result 类型中的一个。

文件名:src/lib.rs

#![allow(unused)]
fn main() {
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --跳过--
}

fn function2() -> IoResult {
    // --跳过--
}
}

清单 7-16:将某个类型纳入作用域时,使用 as 关键字对其重命名

在第二个 use 语句中,我们为 std::io::Result 类型,选择了新名字 IoResult,这不会与我们同时引入该作用域的 std::fmt 中的 Result 冲突。清单 7-15 和清单 7-16 都被认为是惯用的,因此选择由咱们自己决定!

使用 pub use 重新导出名字

Re-exporting Names with pub use

当我们使用 use 关键字将名字纳入作用域时,新作用域中可用的这个名字是私有的。为了使调用咱们代码的代码,就好像这个名字是在咱们代码作用域中定义的那样,引用这个名字,我们可以将 pubuse 结合使用。这种技术被称为 再导出,re-exporting,因为我们在将某个项目纳入作用域的同时,还让其他人可以将该项目纳入他们的作用域了。

清单 7-17 展示清单 7-11 中的代码,其中根模组中的 use 已更改为 pub use

文件名:src/lib.rs

#![allow(unused)]
fn main() {
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_too_waitlist() 路径,来调用 add_to_waitlist 函数,这还需要 front_of_house 模组被标记为 pub。现在,这个 pub use 已经从该根模组中,重新导出了 hosting 模组,外部代码就可以使用 restaurant::hosting::add_to_waitlist() 路径了。

当咱们代码的内部结构,与调用咱们代码的程序员对有关领域的理解有差别时,重新导出就非常有用。例如,在这个餐馆的比方中,经营餐馆的人考虑的是“前厅”和“后厨”。但光顾餐厅的顾客,可能不会从这些角度来考虑餐厅的各个部分。通过 pub use,我们可以一种结构编写代码,而暴露出不同的结构。这样做可以使我们的程序库井井有条,便于编写这个库的程序员,也便于调用这个库的程序员。我们将在第 14 章的 “使用 pub use 导出好用的公开 API” 小节,介绍 pub use 的另一个示例,以及他对代码箱文档的影响。

使用外部包

Using External Packages

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

文件名:Cargo.toml

rand = `0.8.5`

Cargo.toml 中将 rand 添加为依赖项后,Cargo 就会从 crates.io 下载 rand 包和任何依赖项,并将 rand 提供到咱们的项目。

然后,为了将 rand 定义带入咱们包的作用域,我们添加了以该代码箱名字 rand 开头的 use 行,并列出了咱们打算带入作用域的那些项目。清回想一下,在第 2 章 “生成随机数” 小节中,我们将 Rng 特质引入了作用域,并调用了 rand::thread_rng 函数:

use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_rang(1..=100);
}

Rust 社区成员在 crates.io 上,已经提供了许多包,将其中任何包拉入咱们的包,都需要这些同样步骤:在咱们包的 Cargo.toml 文件中列出他们,并使用 use 将他们代码箱中的项目,引入到作用域。

请注意,标准库 std 同样属于相对于咱们包外部的一个代码箱。因为标准库是随 Rust 语言一起提供的,所以我们不需要修改 Cargo.toml 来包含 std。但我们确实需要用 use 来引用他,以便将其中的项目引入咱们包的作用域。例如,对于 HashMap 我们可以使用这行:

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

这是个以 std(标准库代码箱名字)开头的绝对路径。

使用嵌套路径清理大型 use 清单

Using Nested Paths to Clean Up Large use Lists

如果我们使用的是定义在同一代码箱,或同一模组中的多个项目,而将每个项目单独列一行,就会占用文件中的大量垂直空间。例如,我们在 清单 2-4 的猜谜游戏中,使用的这两条 use 语句,将 std 中的项目引入了作用域:

文件名:src/main.rs

#![allow(unused)]
fn main() {
// --跳过--
use std::cmp::Ordering;
use std::io;
// --跳过--
}

实际上,我们可以使用嵌套路径,在一行中将同样这些项目纳入作用域。具体做法是指定出路径的共同部分,后面加两个冒号,然后在这些项目的路径不同部分列表周围,加上花括号,如清单 7-18 所示。

文件名:src/main.rs

#![allow(unused)]
fn main() {
// --跳过--
use std::{cmp::Ordering, io};
// --跳过--
}

清单 7-18:指定嵌套路径,将具有同样前缀的多个项目纳入作用域

在大型程序中,使用嵌套路径将同一代码箱或同一模组中的许多项目引入作用域,可以大大减少所需的单独 use 语句的数量!

在路径的任何层级,我们都可以使用嵌套路径,在对共用子路径的两个 use 语句加以组合时,这尤为有用。例如,清单 7-19 给出了两个 use 语句:一个将 std::io 引入作用域,另一个将 std::io::Write 引入作用域。

文件名:src/lib.rs

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

清单 7-19:两个 use 语句,其中一个是另一个的子路径

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

文件名:src/main.rs

#![allow(unused)]
fn main() {
use std::io::{self, Write};
}

清单 7-20:将清单 7-19 中的路径合并为一个 use 语句

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

全局操作符,*

The Glob Operator

如果我们想将某个路径中定义的 所有 公开项目都纳入作用域,可以在指定路径后加上 * 这个全局操作符:

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

这条 use 语句会将 std::collections 中定义的所有公开项目引入当前作用域。使用全局操作符时要当心!全局性会使咱们更难分辨,哪些名称是在作用域中,以及程序中使用的名字是在何处定义。

全局操作符通常在测试时使用,以便将所有被测试内容引入 tests 模块;我们将在第 11 章 “如何编写测试” 小节中讨论这个问题。全局操作符有时也作为前奏模式,the prelude pattern,的一部分使用:有关该模式的更多信息,请参阅 标准库文档

Last change: 2024-10-07, commit: 7fcba01