模块树中条目的引用路径

为了向 Rust 展示在模块树中找到某个条目的位置,我们使用路径,这与在文件系统中导航时使用路径的方式相同。要调用函数,我们需要知道它的路径。

路径可以采用两种形式

  • 绝对路径是从 crate 根开始的完整路径;对于来自外部 crate 的代码,绝对路径以 crate 名称开头,而对于来自当前 crate 的代码,它以字面量 crate 开头。
  • 相对路径从当前模块开始,并使用 selfsuper 或当前模块中的标识符。

绝对路径和相对路径后都跟有一个或多个标识符,用双冒号 (::) 分隔。

回到示例 7-1,假设我们要调用 add_to_waitlist 函数。这与询问以下问题相同:add_to_waitlist 函数的路径是什么?示例 7-3 包含示例 7-1,其中删除了一些模块和函数。

我们将展示两种从 crate 根中定义的新函数 eat_at_restaurant 调用 add_to_waitlist 函数的方法。这些路径是正确的,但仍然存在另一个问题,这将阻止此示例按原样编译。我们将在稍后解释原因。

eat_at_restaurant 函数是我们库 crate 的公共 API 的一部分,因此我们用 pub 关键字标记它。在 “使用 pub 关键字暴露路径”部分,我们将更详细地介绍 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。你可以想象一个具有相同结构的文件系统:我们将指定路径 /front_of_house/hosting/add_to_waitlist 来运行 add_to_waitlist 程序;使用 crate 名称从 crate 根开始就像在 shell 中使用 / 从文件系统根开始一样。

我们第二次在 eat_at_restaurant 中调用 add_to_waitlist 时,我们使用相对路径。该路径以 front_of_house 开头,front_of_house 模块与 eat_at_restaurant 位于模块树的同一级别。这里,文件系统的等效项将是使用路径 front_of_house/hosting/add_to_waitlist。以模块名称开头意味着该路径是相对的。

选择使用相对路径还是绝对路径是您将根据您的项目做出的决定,这取决于您是更可能将条目定义代码与使用该条目的代码分开移动还是一起移动。例如,如果我们移动了 front_of_house 模块和 eat_at_restaurant 函数到一个名为 customer_experience 的模块中,我们将需要更新到 add_to_waitlist 的绝对路径,但相对路径仍然有效。但是,如果我们将 eat_at_restaurant 函数单独移动到一个名为 dining 的模块中,则到 add_to_waitlist 调用的绝对路径将保持不变,但相对路径将需要更新。我们通常的偏好是指定绝对路径,因为我们更可能希望独立于彼此移动代码定义和条目调用。

让我们尝试编译示例 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 是私有的。换句话说,我们有模块 hosting 和函数 add_to_waitlist 的正确路径,但 Rust 不允许我们使用它们,因为它无法访问私有部分。在 Rust 中,所有条目(函数、方法、结构体、枚举、模块和常量)默认对其父模块是私有的。如果您想使函数或结构体等条目私有,请将其放在模块中。

父模块中的条目不能使用子模块内的私有条目,但子模块中的条目可以使用其祖先模块中的条目。这是因为子模块包装并隐藏了它们的实现细节,但子模块可以看到它们被定义的上下文。为了继续我们的比喻,将隐私规则想象成餐厅的后台办公室:那里发生的事情对餐厅顾客是私有的,但办公室经理可以看到并做他们在餐厅运营中的一切事情。

Rust 选择让模块系统以这种方式运行,以便隐藏内部实现细节是默认设置。这样,您就可以知道您可以更改内部代码的哪些部分而不会破坏外部代码。但是,Rust 确实为您提供了通过使用 pub 关键字使条目公开,从而将子模块代码的内部部分暴露给外部祖先模块的选项。

使用 pub 关键字暴露路径

让我们回到示例 7-4 中的错误,该错误告诉我们 hosting 模块是私有的。我们希望父模块中的 eat_at_restaurant 函数能够访问子模块中的 add_to_waitlist 函数,因此我们用 pub 关键字标记 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:声明 hosting 模块为 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,我们就能访问 hosting。但是 hosting内容仍然是私有的;使模块公开并不会使其内容公开。模块上的 pub 关键字仅允许其祖先模块中的代码引用它,而不能访问其内部代码。因为模块是容器,所以仅通过使模块公开我们无法做太多事情;我们需要更进一步,并选择使模块内的一个或多个条目也公开。

示例 7-6 中的错误消息说 add_to_waitlist 函数是私有的。隐私规则也适用于结构体、枚举、函数和方法以及模块。

让我们也通过在 add_to_waitlist 函数的定义之前添加 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 hostingfn 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_restaurantfront_of_house 是兄弟姐妹),我们可以从 eat_at_restaurant 引用 front_of_house。接下来是用 pub 标记的 hosting 模块。我们可以访问 hosting 的父模块,所以我们可以访问 hosting。最后,add_to_waitlist 函数用 pub 标记,我们可以访问它的父模块,所以这个函数调用有效!

在相对路径中,逻辑与绝对路径相同,只是第一步除外:路径不是从 crate 根开始,而是从 front_of_house 开始。front_of_house 模块定义在与 eat_at_restaurant 相同的模块中,因此从定义 eat_at_restaurant 的模块开始的相对路径有效。然后,由于 hostingadd_to_waitlistpub 标记,路径的其余部分有效,并且此函数调用是有效的!

如果您计划共享您的库 crate 以便其他项目可以使用您的代码,那么您的公共 API 是您与您的 crate 用户之间的合同,它决定了他们如何与您的代码交互。围绕管理对您的公共 API 的更改以使其更容易让人依赖您的 crate 有许多考虑因素。这些考虑因素超出了本书的范围;如果您对此主题感兴趣,请参阅 Rust API 指南

带有二进制文件和库的包的最佳实践

我们提到过一个包可以同时包含 src/main.rs 二进制 crate 根和 src/lib.rs 库 crate 根,并且两个 crate 默认都将具有包名称。通常,具有这种包含库 crate 和二进制 crate 模式的包将在二进制 crate 中只有足够的代码来启动一个可执行文件,该可执行文件调用库 crate 中的代码。这使其他项目可以从包提供的大部分功能中受益,因为库 crate 的代码可以共享。

模块树应在 src/lib.rs 中定义。然后,任何公共条目都可以在二进制 crate 中使用,方法是以包的名称开头路径。二进制 crate 成为库 crate 的用户,就像完全外部的 crate 将使用库 crate 一样:它只能使用公共 API。这有助于您设计一个好的 API;您不仅是作者,还是客户端!

第 12 章中,我们将通过一个命令行程序演示这种组织实践,该程序将同时包含二进制 crate 和库 crate。

super 开头的相对路径

我们可以构建以父模块而不是当前模块或 crate 根开始的相对路径,方法是在路径的开头使用 super。这就像使用 .. 语法开始文件系统路径一样。使用 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 将结构体和枚举指定为公共的,但是对于 pub 与结构体和枚举的用法,有一些额外的细节。如果我们在结构体定义之前使用 pub,我们会使结构体公开,但结构体的字段仍然是私有的。我们可以根据具体情况使每个字段公开或不公开。在示例 7-9 中,我们定义了一个公共的 back_of_house::Breakfast 结构体,它有一个公共的 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 字段。请注意,我们不能在 eat_at_restaurant 中使用 seasonal_fruit 字段,因为 seasonal_fruit 是私有的。尝试取消注释修改 seasonal_fruit 字段值的行,看看你会得到什么错误!

另外,请注意,由于 back_of_house::Breakfast 具有私有字段,因此该结构体需要提供一个公共的关联函数来构造 Breakfast 的实例(我们在这里将其命名为 summer)。如果 Breakfast 没有这样的函数,我们就无法在 eat_at_restaurant 中创建 Breakfast 的实例,因为我们无法在 eat_at_restaurant 中设置私有 seasonal_fruit 字段的值。

相反,如果我们使枚举公开,则其所有变体都是公开的。我们只需要在 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:将枚举指定为公共会使其所有变体都公开

由于我们使 Appetizer 枚举公开,因此我们可以在 eat_at_restaurant 中使用 SoupSalad 变体。

枚举除非其变体是公共的,否则它们不是很有用;在每种情况下都必须用 pub 注释所有枚举变体会很烦人,因此枚举变体的默认设置是公共的。结构体通常在字段不是公共的情况下也很有用,因此结构体字段遵循一切默认都是私有的通用规则,除非用 pub 注释。

还有一个涉及 pub 的情况我们尚未涵盖,那就是我们的最后一个模块系统功能:use 关键字。我们将首先单独介绍 use,然后再展示如何组合 pubuse