这里继续沿用上次工程rust-demo
不得不写出调用函数的路径可能会感到不方便和重复。在之前的例子中,无论我们选择add_to_waitlist函数的绝对路径还是相对路径,每次我们想调用add_to_waitlist时,我们都必须指定front_of_house和hosting。幸运的是,有一种方法可以简化这个过程:我们可以用use关键字创建一个路径的快捷方式,然后在作用域中的其他地方使用更短的名称。
在下例种,我们将crate::front_of_house::hosting模块纳入eat_at_restaurant函数的范围,因此我们只需指定hosting::add_to_waitlist来调用eat_at_restaurant中的add_to_waitlist函数。
文件名:src/lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting; // use关键字引入crate::front_of_hosue::hosting模块
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
在作用域中添加使用和路径类似于在文件系统中创建符号链接。通过在箱crate根中添加use crate::front _ of _ house::hosting,hosting现在是该范围内的有效名称,就像在箱crate根中定义了托管模块一样。与任何其他路径一样,被纳入使用范围的路径也会检查隐私。
请注意,use只为发生使用的特定范围创建快捷方式。下例种将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 { // 模块customer
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}
编译器错误显示快捷方式在客户模块中不再适用:
$ 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`
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` due to previous error; 1 warning emitted
请注意,还有一个警告,该use不再用于其作用域!要解决这个问题,也在customer模块内移动use,或者在子customer模块内用super::hosting引用父模块中的快捷方式。
在之前的例子中,你可能想知道为什么我们指定use crate::front _ of _ house::hosting,然后在eat_at_restaurant中调用hosting::add_to_waitlist,而不是指定一直到add_to_waitlist函数的use路径来达到相同的结果,如下例
文件名:src/lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting::add_to_waitlist; // use指定到具体的函数
pub fn eat_at_restaurant() {
add_to_waitlist();
}
尽管之前的例子和上面的例子完成了相同的任务,但是之前的例子是将函数引入use范围的惯用方式。将函数的父模块纳入use范围意味着我们必须在调用函数时指定父模块。在调用函数时指定父模块可以清楚地表明函数不是本地定义的,同时还可以最大限度地减少完整路径的重复。上面的例子中的代码不清楚add_to_waitlist是在哪里定义的。
另一方面,当使用use引入结构、枚举和其他项时,习惯上指定完整路径。下例种显示了将标准库的HashMap结构引入二进制crate的惯用方法。
文件名:src/main.rs
use std::collections::HashMap; // use标准库HashMap
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
这个习惯用法背后没有什么强有力的理由:这只是已经出现的惯例,人们已经习惯了以这种方式阅读和编写Rust代码。
这个习惯用法的例外是,如果我们用use语句将两个同名的项目带入作用域,因为Rust不允许这样做。下例种显示了如何将两个同名但父模块不同的Result类型引入作用域,以及如何引用它们。
文件名:src/lib.rs
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
Ok(())
}
正如您所看到的,使用父模块区Result类型。如果我们指定use std::fmt::Result和use std::io::Result,我们将在同一个范围内有两种Result类型,Rust在使用Result时不知道我们指的是哪一种。
这里有另一个解决方法,可以将两个同名的类型放在同一个作用域中use:在路径后面,我们可以为类型指定as和一个新的本地名称或别名。下例显示了编写上例中代码的另一种方法,用as重命名两种Result类型中的一种。
文件名:src/lib.rs
use std::fmt::Result;
use std::io::Result as IoResult; // as关键字,别名
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
在第二个use语句中,我们为std::io::Result类型选择了新名称IoResult,它不会与我们纳入范围的std::fmt的结果冲突。
当我们用use关键字将一个名字带入作用域时,在新作用域中可用的名字是私有的。为了使调用我们的代码的代码能够引用该名称,就好像它已经在该代码的作用域中定义了一样,我们可以将pub和use结合起来。这种技术被称为重新导出,因为我们将一个项目纳入范围,但同时也让其他人可以将该项目纳入他们的范围。
下例中将根模块中的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 use
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
在此更改之前,外部代码必须使用路径restaurant::front _ of _ house::hosting::add_to_waitlist()调用add _ to _ wait list函数。既然这个pub use已经从根模块中重新导出了主机模块,那么外部代码现在可以使用路径restaurant::hosting::add _ to _ wait list()来代替。
当您的代码的内部结构与调用您的代码的程序员考虑领域的方式不同时,重新导出是有用的。例如,在这个餐馆的比喻中,经营餐馆的人会想到“房子的前面”和“房子的后面”但是光顾餐馆的顾客可能不会用这些术语来考虑餐馆的各个部分。通过pub use,我们可以用一种结构编写代码,但公开不同的结构。这样做使得我们的库对于在库上工作的程序员和调用库的程序员来说组织得很好。
在之前的章节中,我们编写了一个猜谜游戏项目,它使用一个名为rand的外部包来获得随机数。为了在我们的项目中使用rand,我们将这一行添加到Cargo.toml:
文件名:Cargo.toml
rand = "0.8.3"
在Cargo.toml中添加rand作为依赖项,告诉Cargo从crates.io下载rand包和任何依赖项,并使rand可用于我们的项目。
然后,为了将rand定义纳入我们的包的范围,我们添加了一个use行,以箱crate的名称rand开始,并列出我们想要纳入范围的项目。如下例:
use std::io;
use rand::Rng; // 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将项目从它们的crates放入范围。
注意,标准的std库也是我们包外部的一个crate。因为Rust语言附带了标准库,所以我们不需要修改Cargo.toml来包含std。但是我们确实需要用use来引用它,以便将项目从那里带入我们的包的范围。例如,对于HashMap,我们可以使用下面这行代码:
use std::collections::HashMap;
这是一个以std开始的绝对路径,即标准库箱的名称。
如果我们在同一个crate或者同一个模块中定义了多个项目,那么在单独的一行中列出每个项目会占用我们文件中大量的垂直空间。例如,我们在之前章节的的猜谜游戏中使用的两个use语句将项目从std带入范围:
文件名:src/main.rs
use rand::Rng; // rand库
// --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!"),
}
}
相反,我们可以使用嵌套路径将相同的项目放在一行中。为此,我们指定路径的公共部分,后跟两个冒号,然后用花括号括起不同路径部分的列表,
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!"),
}
}
在更大的程序中,使用嵌套路径从同一个板条箱或模块中引入许多项可以减少大量需要的单独use语句的数量!
我们可以在路径中的任何级别使用嵌套路径,这在组合共享子路径的两个use语句时非常有用。例如,下例中显示了两条use语句:一条将std::io引入作用域,另一条将std::io::Write引入作用域。
文件名:src/lib.rs
use std::io;
use std::io::Write; // std::io下的包
这两条路径的共同部分是std::io,这是完整的第一条路径。要将这两条路径合并成一条use语句,我们可以在嵌套路径中使用self,如下例所示:
文件名:src/lib.rs
use std::io::{self, Write}; // self
这一行将std::io和std::io::Write引入范围。
如果我们希望将路径中定义的所有公共项都纳入范围,我们可以指定该路径后跟* 运算符:
use std::collections::*; // *通配符
这里的use语句将std::collections中定义的所有公共项都纳入当前范围。使用通配运算符时要小心!通配运算符使得区分什么名字在作用域内以及程序中使用的名字是在哪里定义的变得更加困难。
通配运算符通常在测试时使用,用于将所有被测内容放入测试模块。