用于引用模块树中项的路径
为了向 Rust 展示在模块树中的位置可以找到项目,我们使用路径的方式与导航文件系统时使用路径的方式相同。要调用一个函数,我们需要知道它的路径。
路径可以采用两种形式:
绝对路径是从 crate 根开始的完整路径;对于来自外部 crate 的代码,绝对路径以 crate 名称开头,对于来自当前 crate 的代码,它以文字crate开头。
相对路径从当前模块开始,使用self、super或当前模块中的标识符。
绝对路径和相对路径后跟一个或多个标识符,这些标识符用双冒号 (::) 分隔。
回到示例 7-1,假设我们想调用 add_to_waitlist 函数。这与问:add_to_waitlist 函数的路径是什么相同?示例 7-3 包含示例 7-1,其中删除了一些模块和函数。
我们将展示两种从新函数调用 add_to_waitlist 函数的方法
eat_at_restaurant,在 crate 根中定义。这些路径是正确的,但仍然存在另一个问题,这将阻止此示例按原样编译。我们稍后会解释原因。
eat_at_restaurant 函数是我们库 crate 的公共 API 的一部分,因此我们使用 pub 关键字来标记它。在“使用 pub 公开路径
Keyword“部分,我们将更详细地介绍 pub。
文件名: src/lib.rs
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
示例 7-3:使用绝对路径和相对路径调用 add_to_waitlist 函数
我们第一次在 eat_at_restaurant 中调用 add_to_waitlist 函数时,我们使用绝对路径。add_to_waitlist 函数定义在与 eat_at_restaurant 相同的 crate 中,这意味着我们可以使用 crate 关键字来启动绝对路径。然后,我们将每个连续的模块包括进来,直到我们进入 add_to_waitlist。你可以想象一个具有相同结构的文件系统:我们将指定运行 add_to_waitlist 程序的路径 /front_of_house/hosting/add_to_waitlist ;使用 crate 名称从 crate 根开始,就像在 shell 中使用 / 从 Filesystem 根开始。
第二次在 eat_at_restaurant 中调用 add_to_waitlist 时,我们使用相对路径。该路径以 front_of_house 开头,该模块的名称在与 eat_at_restaurant 相同的模块树级别定义。这里
文件系统等效的是使用路径
front_of_house/hosting/add_to_waitlist 。以 module name 开头表示 path 是相对的。
选择使用相对路径还是绝对路径是您将根据项目做出的决定,这取决于您更有可能将项目定义代码与使用该项目的代码分开移动还是与使用该项目的代码一起移动。例如,如果我们将 front_of_house 模块和
eat_at_restaurant函数转换为名为 customer_experience 的模块,我们需要将绝对路径更新为 add_to_waitlist,但相对路径仍然有效。但是,如果我们将 eat_at_restaurant 函数单独移动到名为 dining 的模块中,则
add_to_waitlist调用将保持不变,但需要更新相对路径。我们通常倾向于指定绝对路径,因为更有可能的是,我们希望将代码定义和 item 调用彼此独立地移动。
让我们尝试编译示例 7-3 并找出为什么它还不能编译!我们得到的错误如示例 7-4 所示。
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
error[E0603]: module `hosting` is private
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
示例 7-4:构建示例 7-3 中的代码时出现编译器错误
错误消息指出模块托管是私有的。换句话说,我们有 hosting module 和 add_to_waitlist 的正确路径
函数,但 Rust 不允许我们使用它们,因为它无法访问
private 部分。在 Rust 中,所有项目(函数、方法、结构、枚举、
modules 和 constants)是父模块的私有。如果需要帮助,
要将 function 或 struct 等项设为私有,请将其放在 Module 中。
父模块中的项不能使用子模块中的私有项,但子模块中的项可以使用其祖先模块中的项。这是因为子模块会包装和隐藏其实现详细信息,但子模块可以看到定义它们的上下文。继续我们的比喻,将隐私规则想象成餐厅的后台办公室:那里发生的事情对餐厅客户来说是私人的,但办公室经理可以看到他们经营的餐厅并做任何事情。
Rust 选择让 module system 以这种方式运行,因此隐藏内部实现细节是默认的。这样,您就知道可以在不破坏外部代码的情况下更改内部代码的哪些部分。但是,Rust 确实为您提供了通过使用 pub 关键字将项目公开来将子模块代码的内部部分暴露给外部祖先模块的选项。
使用 pub 关键字公开路径
让我们回到示例 7-4 中的错误,它告诉我们 hosting 模块是私有的。我们希望父模块中的 eat_at_restaurant 函数能够访问子模块中的 add_to_waitlist 函数,因此我们将
hosting 模块,如示例 7-5 所示。
文件名: src/lib.rs
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
示例 7-5:将托管模块声明为 pub 以从 eat_at_restaurant 使用它
不幸的是,示例 7-5 中的代码仍然会导致编译器错误,如示例 7-6 所示。
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:9:37
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:12:30
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
示例 7-6:构建示例 7-5 中的代码时出现的编译器错误
发生了什么事?在 mod hosting 前面添加 pub 关键字会使模块公开。通过此更改,如果我们可以访问 front_of_house,我们就可以访问托管。但托管的内容仍然是私有的;将模块设为公共不会使其内容公开。模块上的 pub 关键字只允许其祖先模块中的代码引用它,而不允许访问其内部代码。因为模块是容器,所以我们只通过公开模块来做不了什么;我们需要更进一步,选择将模块中的一个或多个项目也设为 public。
示例 7-6 中的错误表明 add_to_waitlist 函数是 private。隐私规则适用于结构、枚举、函数和方法以及模块。
我们还通过添加 pub 来将 add_to_waitlist 函数设为公共
关键字,如示例 7-7 所示。
文件名: src/lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
示例 7-7:将 pub 关键字添加到 mod 托管
fn add_to_waitlist 让我们从
eat_at_restaurant
现在代码可以编译了!为了了解为什么添加 pub 关键字可以让我们在隐私规则方面eat_at_restaurant使用这些路径,让我们看看绝对路径和相对路径。
在绝对路径中,我们从 crate 开始,它是 crate 模块树的根。front_of_house 模块在 crate 根中定义。而
front_of_house 不是公共的,因为 eat_at_restaurant 函数在与 front_of_house 相同的模块中定义(即 eat_at_restaurant
front_of_house是兄弟姐妹),我们可以front_of_house从
eat_at_restaurant。接下来是标有 pub 的 hosting 模块。我们可以访问 hosting 的父模块,因此我们可以访问 hosting。最后,
add_to_waitlist函数都标有 pub,我们可以访问它的父模块,所以这个函数调用是有效的!
在相对路径中,逻辑与绝对路径相同,只是
第一步:路径不是从 crate 根开始,而是从
front_of_house。front_of_house 模块在与 eat_at_restaurant 相同的模块中定义,因此从其中的模块开始的相对路径
eat_at_restaurant 是定义的工作。然后,因为托管和
add_to_waitlist标记为 pub,则路径的其余部分有效,并且此函数调用有效!
如果您计划共享库 crate,以便其他项目可以使用您的代码,则您的公共 API 是您与 crate 用户的合同,用于确定他们如何与您的代码交互。关于管理对公有 API 的更改,人们可以更轻松地依赖您的 crate,需要考虑许多因素。这些考虑超出了本书的范围;如果您对此主题感兴趣,请参阅 Rust API 指南。
具有二进制文件和库的包的最佳实践
我们提到过,一个包可以同时包含一个 src/main.rs 二进制 crate 根和一个 src/lib.rs 库 crate 根,并且两个 crate 都默认具有包名。通常,具有这种同时包含库和二进制 crate 的模式的包在二进制 crate 中的代码刚好足以启动调用库 crate 中的代码的可执行文件。这使其他项目可以从包提供的大部分功能中受益,因为库 crate 的代码可以共享。
模块树应在 src/lib.rs 中定义。然后,任何公共项目都可以在二进制 crate 中使用,只需以包的名称开始 paths 即可。二进制 crate 成为库 crate 的用户,就像一个完全外部的 crate 使用库 crate 一样:它只能使用公共 API。这有助于您设计一个好的 API;您不仅是作者,还是客户!
在第 12 章中,我们将使用一个命令行程序来演示这种组织实践,该程序将同时包含一个二进制 crate 和一个库 crate。
使用 super 开始相对路径
我们可以通过在路径的开头使用 super 来构造从父模块开始的相对路径,而不是当前模块或 crate 根。这就像使用 .. 语法启动文件系统路径一样。用
super 允许我们引用我们知道在父模块中的项目,当模块与父模块密切相关但父模块有一天可能会移动到模块树中的其他位置时,这可以使重新排列模块树更容易。
考虑示例 7-8 中的代码,它模拟了厨师修复错误订单并亲自将其带给客户的情况。back_of_house 模块中定义的函数 fix_incorrect_order deliver_order通过指定
deliver_order,从 super 开始。
文件名: src/lib.rs
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
示例 7-8:使用以 super 开头的相对路径调用函数
fix_incorrect_order 函数在 back_of_house 模块中,因此我们可以使用 super 转到 back_of_house 的父模块,在本例中是 crate,即根。从那里,我们寻找deliver_order并找到它。成功!我们认为 back_of_house 模块和 deliver_order 函数可能会保持相同的关系,如果我们决定重新组织 crate 的模块树,它们可能会一起移动。因此,我们使用了 super,这样如果这些代码被移动到不同的模块,我们将来更新代码的地方会更少。
将结构和枚举设为公共
我们也可以使用 pub 将结构和枚举指定为 public,但将 pub 与结构和枚举一起使用还有一些额外的细节。如果我们使用 pub
在结构体定义之前,我们将结构体设为公共,但结构体的字段
仍将是私有的。我们可以根据具体情况将每个字段设为公开或不公开
基础。在示例 7-9 中,我们定义了一个 public back_of_house::Breakfast 结构体,该结构体具有一个 public toast 字段,但有一个私有 seasonal_fruit 字段。这模拟了一家餐厅的情况,顾客可以选择随餐吃的面包类型,但厨师根据时令和库存来决定哪些水果搭配这顿饭。可用的水果变化很快,因此客户无法选择水果,甚至无法看到他们将获得哪种水果。
文件名: src/lib.rs
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
// Order a breakfast in the summer with Rye toast
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal
// meal.seasonal_fruit = String::from("blueberries");
}
示例 7-9:具有一些公共字段和一些私有字段的结构体
因为 back_of_house::Breakfast 结构体中的 toast 字段是公共的,所以在 eat_at_restaurant 中,我们可以使用点表示法对 toast 字段进行写入和读取。请注意,我们不能seasonal_fruit在
eat_at_restaurant,因为 seasonal_fruit 是私有的。尝试取消注释修改 seasonal_fruit 字段值的行,以查看您得到的错误!
另外,请注意,由于 back_of_house::Breakfast 有一个私有字段,因此结构体需要提供一个公共关联函数来构造 Breakfast 的实例(我们在这里将其命名为 summer)。如果 Breakfast 没有这样的函数,我们就无法在
eat_at_restaurant,因为我们无法设置私有
seasonal_fruit 字段中eat_at_restaurant。
相反,如果我们将枚举设为 public,则其所有变体都是 public。我们只需要在 enum 关键字之前放置 pub,如示例 7-10 所示。
文件名: src/lib.rs
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
示例 7-10:将枚举指定为 public 会使其所有变体都公开
因为我们公开了 Appetizer 枚举,所以我们可以使用 Soup and Salad
变体eat_at_restaurant。
除非它们的变体是公开的,否则枚举不是很有用;在每种情况下都必须用 pub 注释所有枚举变体会很烦人,因此枚举变体的默认值是 public。结构体通常在其字段不是公共的的情况下很有用,因此结构体字段遵循默认所有内容都是私有的一般规则,除非用 pub 进行注释。
还有一种涉及 pub 的情况我们没有介绍,那就是我们最后一个模块系统功能:use 关键字。我们将首先介绍 use 本身,然后我们将展示如何组合 pub 和 use。