键盘快捷键

按 ← 或 → 在章节间切换

按 S 或 / 搜索本书

按 ? 显示此帮助

按 Esc 隐藏此帮助

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

调用函数时必须写出完整路径,可能会显得不方便且重复。在示例 7-7 中,无论选择 add_to_waitlist 函数的绝对路径还是相对路径,每次调用它时都必须同时写出 front_of_house 和 hosting。幸运的是,我们可以简化这一过程:使用 use 关键字为路径创建一次快捷方式,之后便可以在该作用域的其他位置使用较短的名称。

在示例 7-11 中,我们将 crate::front_of_house::hosting 模块引入 eat_at_restaurant 函数的作用域,因此在该函数中只需指定 hosting::add_to_waitlist,即可调用 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 只会在它所在的特定作用域中创建快捷方式。示例 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]: cannot find module or crate `hosting` in this scope
  --> src/lib.rs:11:9
   |
11 |         hosting::add_to_waitlist();
   |         ^^^^^^^ use of unresolved module or unlinked crate `hosting`
   |
   = help: if you wanted to use a crate named `hosting`, use `cargo add hosting` to add it to your `Cargo.toml`
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)]` (part of `#[warn(unused)]`) 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 类型;使用 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--
    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,我们可以按一种结构编写代码,同时公开另一种结构。这样,库内部代码的组织对库的开发者来说很清晰,而库的调用者也能方便地使用它。第 14 章的“导出便捷的公共 API”一节将展示另一个 pub use 示例,以及它对 crate 文档的影响。

使用外部包

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

文件名:Cargo.toml
rand = "0.10.1"

在 Cargo.toml 中将 rand 添加为依赖,会告诉 Cargo 从 crates.io 下载 rand 包及其依赖,并让我们的项目能够使用 rand。

随后,为了将 rand 中的定义引入包的作用域,我们添加了一条以 crate 名称 rand 开头的 use 语句,并列出想要引入作用域的项目。回想第 2 章“生成随机数”一节,我们将 rand::prelude 模块中的项目引入作用域,并调用了 rand::rng 函数:

use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_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(标准库 crate 的名称)开头的绝对路径。

使用嵌套路径简化 use 列表

如果要使用同一 crate 或同一模块中定义的多个项目,将每个项目分别列在单独一行,会占用文件中大量垂直空间。例如,示例 2-4 的猜数字游戏中有两条 use 语句,将 std 中的项目引入作用域:

文件名:src/main.rs
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_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::prelude::*;
// --snip--
use std::{cmp::Ordering, io};
// --snip--

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_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 运算符有时也会作为预导入模式的一部分使用;更多信息请参阅标准库文档。