使用 super 开始相对路径

也可以使用 super 开头来构建相对路径。这么做类似于文件系统中以 … 开头:该路径从 父 模块开始而不是当前模块。这在例如示例 7-9 这样的情况下有用处,在这里 clarinet 函数通过指定以 super 开头的路径调用 breathe_in 函数:

文件名: src/lib.rs

# fn main() {}
#
mod instrument {
 fn clarinet() {
 super::breathe_in(); }
}
fn breathe_in() {
 // 函数体
}

示例 7-9: 使用以 super 开头的路径从父目录开始调用函数

clarinet 函数位于 instrument 模块中,所以可以使用 super 进入 instrument 的父模块,也就是根 crate。从这里可以找到 breathe_in。成功!

你可能想要使用 super 开头的相对路而不是以 crate 开头的绝对路径的原因是 super 可能会使修改有着不同模块层级结构的代码变得更容易,如果定义项和调用项的代码被一同移动的话。例如,如果我们决定将 instrument 模块和 breathe_in 函数放入 sound 模块中,这时我们只需增加 sound 模块即可,如示例 7-10 所示。

文件名: src/lib.rs

mod sound {
 mod instrument {
 fn clarinet() {
 super::breathe_in(); }
 }
 fn breathe_in() {
 // 函数体
 }
}

示例 7-10: 增加一个名为 sound 的父模块并不影响相对路径 super::breathe_in

示例 7-10 在 clarinet 函数中调用 super::breathe_in 将如示例 7-9 一样继续有效,无需更新路径。如果在 clarinet 函数不使用 super::breathe_in 而是使用 crate::breathe_in 的话,当增加父模块 sound 后,则需要更新 clarinet 函数使用 crate:🔉:breathe_in 路径。使用相对路径可能意味着重新
布局模块时需要更少的必要修改。

对结构体和枚举使用 pub

可以以模块与函数相同的方式来设计公有的结构体和枚举,不过有一些额外的细节。

如果在结构体定义中使用 pub,可以使结构体公有。然而结构体的字段仍是私有的。可以在每一个字段的基准上选择其是否公有。在示例 7-11 中定义了一个公有结构体 plant::Vegetable,其包含公有的 name 字段和私有的 id 字段。

文件名: src/main.rs

mod plant {
 pub struct Vegetable {
 pub name: String,
 id: i32,
 }
 impl Vegetable {
 pub fn new(name: &str) -> Vegetable {
 Vegetable {
 name: String::from(name),
 id: 1,
 }
 }
 }
}
fn main() {
 let mut v = plant::Vegetable::new("squash"); v.name = String::from("butternut squash");
 println!("{} are delicious", v.name);
 // 如果将如下行取消注释代码将无法编译:
 // println!("The ID is {}", v.id);
}

示例 7-11: 结构体带有公有和私有的字段

因为 plant::Vegetable 结构体的 name 字段使公有的,在 main 中可以使用点号读写 name 字段。不允许在 main 中使用 id 字段因为其使私有的。尝试取消注释的行来打印 id 字段的值来看看会出现什么错误!另外注意因为 plant::Vegetable 有私有字段,需要提供一个公有的关联函数来构建 Vegetable 的实例(这里使用了传统的名称 new)。如果 Vegetable 没有提供这么一个函数,我们就不能在 main 中创建 Vegetable 的实例,因为在 main 中不允许设置私有字段 id 的值。

相反,如果有一个公有枚举,其所有成员都是公有。只需在 enum 关键词前加上 pub,如示例 7-12 所示。

文件名: src/main.rs

mod menu {
pub enum Appetizer {
 Soup,
 Salad,
 }
}
fn main() {
 let order1 = menu::Appetizer::Soup;
 let order2 = menu::Appetizer::Salad;}

示例 7-12: 将枚举设计为公有会使其所有成员公有
因为 Appetizer 枚举是公有的,可以在 main 中使用 Soup and Salad 成员。

还有一种使用 pub 的场景我们还没有涉及到,而这是我们最后要讲的模块功能:use 关键字。我们先单独介绍 use,然后展示如何结合使用 pub 和 use。

使用 use 关键字将名称引入作用域

你可能考虑过本章很多的函数调用的路径是冗长和重复的。例如示例 7-8 中,当我们选择 clarinet 函数的绝对或相对路径时,每次想要调用 clarinet 时都不得不也指定 sound 和 instrument。幸运的是,有一次性将路径引入作用域然后就像调用本地项那样的方法:使用 use 关键字。在示例 7-13 中将
crate:🔉:instrument 模块引入了 main 函数的作用域,以便只需指定 instrument::clarinet 来调用 clarinet 函数。

文件名: src/main.rs

mod sound {
 pub mod instrument {
 pub fn clarinet() {
 // 函数体
 }
 }
}
use crate::sound::instrument;fn main() {
 instrument::clarinet();
 instrument::clarinet();
 instrument::clarinet();
}

示例 7-13: 使用 use 将模块引入作用域并使用绝对路径来缩短在模块中调用项所必须的路径

在作用域中增加 use 和路径类似于在文件系统中创建软连接(符号连接,symbolic link)。通过在 crate 根增加 use crate:🔉:instrument,现在
instrument 在作用域中就是有效的名称了,如同它被定义于 crate 根一样。现在,既可以使用老的全路径方式取得 instrument 模块的项,也可以使用新的通过 use 创建的更短的路径。通过 use 引入作用域的路径也会检查私有性,同其它路径一样。

如果希望通过 use 和相对路径来将项引入作用域,则与直接通过相对路径调用项有些小的区别:不同于从当前作用域的名称开始,use 中的路径必须以 self

示例 7-14 展示了如何指定相对路径来取得与示例 7-13 中使用绝对路径一样的行为。

文件名: src/main.rs

mod sound {
 pub mod instrument {
 pub fn clarinet() {
 // 函数体
 }
 }
}
use self::sound::instrument;fn main() {
 instrument::clarinet();
 instrument::clarinet();
 instrument::clarinet();
}

示例 7-14: 通过 use 和相对路径将模块引入作用域

当指定 use 后以 self 开头的相对路径在未来可能不是必须的;这是一个开发者正在尽力消除的语言中的不一致。

如果调用项目的代码移动到模块树的不同位置但是定义项目的代码却没有,那么选择使用 use 指定绝对路径可以使更新更轻松,这与示例 7-10 中同时移动的情况相对。例如,如果我们决定采用示例 7-13 的代码,将 main 函数的行为提取到函数 clarinet_trio 中,并将该函数移动到模块 performance_group中,这时 use 所指定的路径无需变化,如示例 7-15 所示。

文件名: src/main.rs

mod sound {
 pub mod instrument {
 pub fn clarinet() {
 // 函数体
 }
 }
}
mod performance_group {
 use crate::sound::instrument; pub fn clarinet_trio() {
 instrument::clarinet();
 instrument::clarinet();
 instrument::clarinet();
 }
}
fn main() {
 performance_group::clarinet_trio();}

示例 7-15: 移动调用项的代码时绝对路径无需移动

相反,如果对示例 7-14 中指定了相对路径的代码做同样的修改,则需要将 use self:🔉:instrument 变为 use super:🔉:instrument。如果你不确定将来模块树会如何变化,那么选择采用相对或绝对路径是否会减少修改可能全靠猜测,不过本书的作者倾向于通过 crate 指定绝对路径,因为定义和调用项的代码更有可能相互独立的在模块树中移动,而不是像示例 7-10 那样一同移动。

use 函数路径使用习惯 VS 其他项

示例 7-13 中,你可能会好奇为什么指定 use crate:🔉:instrument 接着在 main 中调用 instrument::clarinet,而不是如示例 7-16 所示的有相同行为的代码:

文件名: src/main.rs

mod sound {
 pub mod instrument {
 pub fn clarinet() {
 // 函数体
 }
 }
}
use crate::sound::instrument::clarinet;fn main() {
 clarinet();
 clarinet();
 clarinet();
}

示例 7-16: 通过 use 将 clarinet 函数引入作用域,这是不推荐的

对于函数来说,通过 use 指定函数的父模块接着指定父模块来调用方法被认为是习惯用法。这么做而不是像示例 7-16 那样通过 use 指定函数的路径,清楚的表明了函数不是本地定义的,同时仍最小化了指定全路径时的重复。

对于结构体、枚举和其它项,通过 use 指定项的全路径是习惯用法。例如,示例 7-17 展示了将标准库中 HashMap 结构体引入作用域的习惯用法。

文件名: src/main.rs

use std::collections::HashMap;
fn main() {
 let mut map = HashMap::new(); map.insert(1, 2);
 }

示例 7-17: 将 HashMap 引入作用域的习惯用法

相反,示例 7-18 中的代码将 HashMap 的父模块引入作用域不被认为是习惯用法。这个习惯并没有很强制的理由;这是慢慢形成的习惯同时人们习惯于这么读写。

文件名: src/main.rs

use std::collections;
fn main() {
 let mut map = collections::HashMap::new(); map.insert(1, 2);
}

示例 7-18: 将 HashMap 引入作用域的非习惯方法

这个习惯的一个例外是如果 use 语句会将两个同名的项引入作用域时,这是不允许的。示例 7-19 展示了如何将两个不同父模块的 Result 类型引入作用域并引用它们。

文件名: src/lib.rs

use std::fmt;
use std::io;
fn function1() -> fmt::Result {
# Ok(())
}
fn function2() -> io::Result<()> {# Ok(())
}

示例 7-19: 将两个同名类型引入作用域必须使用父模块

因为如果我们指定 use std::fmt::Result 和 use std::io::Result,则作用域中会有两个 Result 类型,Rust 无法知道我们想用哪个 Result。尝试这么做并看看编译器错误!

通过 as 关键字重命名引入作用域的类型

将两个同名类型引入同一作用域这个问题还有另一个解决办法:可以通过在 use 后加上 as 和一个新名称来为此类型指定一个新的本地名称。示例 7-20 展示了另一个编写示例 7-19 中代码的方法,通过 as 重命名了其中一个 Result 类型。

文件名: src/lib.rs

use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
# Ok(())
}
fn function2() -> IoResult<()> {# Ok(())
}

示例 7-20: 通过 as 关键字重命名引入作用域的类型

在第二个 use 语句中,我们选择 IoResult 作为 std::io::Result 的新名称,它与从 std::fmt 引入作用域的 Result 并不冲突。这样做也被认为是惯用的;示例 7-19 还是示例 7-20 全看你的选择。

通过 pub use 重导出名称

当使用 use 关键字将名称导入作用域时,在新作用域中可用的名称是私有的。如果希望调用你编写的代码的代码能够像你一样在其自己的作用域内引用这些类型,可以结合 pub 和 use。这个技术被称为 “重导出”(re-exporting),因为这样做将项引入作用域并同时使其可供其他代码引入自己的作用域。

例如,示例 7-21 展示了示例 7-15 中的代码将 performance_group 的 use 变为 pub use 的版本。

文件名: src/main.rs

mod sound {
 pub mod instrument {
 pub fn clarinet() {
 // 函数体
 }
 }
}
mod performance_group {
 pub use crate::sound::instrument;
 pub fn clarinet_trio() {
 instrument::clarinet();
 instrument::clarinet();
 instrument::clarinet();
 }
}
fn main() {
 performance_group::clarinet_trio();
 performance_group::instrument::clarinet();}

示例 7-21: 通过 pub use 使名称可引入任何代码的作用域中

通过 pub use,现在 main 函数可以通过新路径 performance_group::instrument::clarinet 来调用 clarinet 函数。如果没有指定

pub use,clarinet_trio 函数可以在其作用域中调用 instrument::clarinet 但 main 则不允许使用这个新路径。

使用外部包

在第二章中我们编写了一个猜猜看游戏。那个项目使用了一个外部包,rand,来生成随机数。为了在项目中使用 rand,在 Cargo.toml 中加入了如下行:

文件名: Cargo.toml

[dependencies]rand = "0.5.5"

在 Cargo.toml 中加入 rand 依赖告诉了 Cargo 要从 https://crates.io 下载 rand 和其依赖,并使其可在项目代码中使用。

接着,为了将 rand 定义引入项目包的作用域,加入一行 use,它以 rand 包名开头并列出了需要引入作用域的项。回忆一下第二章的 “生成一个随机数” 部分,我们曾将 Rng trait 引入作用域并调用了 rand::thread_rng 函数:

use rand::Rng;
fn main() {
 let secret_number = rand::thread_rng().gen_range(1, 101);}

https://crates.io 上有很多社区成员发布的包,将其引入你自己的项目涉及到相同的步骤:在 Cargo.toml 列出它们并通过 use 将其中定义的项引入项目包的作用域中。

注意标准库(std)对于你的包来说也是外部 crate。因为标准库随 Rust 语言一同分发,无需修改 Cargo.toml 来引入 std,不过需要通过 use 将标准库中定义的项引入项目包的作用域中来引用它们,比如 HashMap:

use std::collections::HashMap;

这是一个以标注库 crate 名 std 开头的绝对路径。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐