用于引用模组树中项目的路径

Paths for Referring to an Item in the Module Tree

为告诉 Rust 在模组树中的何处,可以找到某个项目,我们就像在文件系统中一样,用到了路径。要调用某个函数,我们需要知道他的路径。

路径有两种形式:

  • 绝对路径,an absolute path,是从代码箱根开始的完整路径;对于外部代码箱中的代码,绝对路径从代码箱名字开始,而对于当前代码箱中的代码,绝对路径从字面的 crate 开始;

  • 相对路径,a relative path 从当前模组开始,并用到 selfsuper 关键字,或当前模组中的某个标识符。

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

回到清单 7-1,假设我们要调用 add_too_waitlist 函数。这等同于在询问:add_to_waitlist 函数的路径是什么?下面清单 7-3 包含了去掉了部分模组及函数的清单 7-1。

我们将展示两种从代码箱根处,定义的新函数 eat_at_restaurant,调用 add_too_waitlist 函数的方法。这两个路径都是正确的,但还存在另一将导致本示例无法按原样编译的问题。我们稍后会解释原因。

eat_at_restaurant 函数,是咱们库代码箱公共 API 的一部分,因此我们使用了 pub 关键字对其进行标记。在 使用 pub 关键字暴露路径 小节,我们将详细介绍 pub

文件名:src/lib.rs

#![allow(unused)]
fn main() {
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 绝对路径方式
    crate::front_of_house::hosting::add_to_waitlist();

    // 相对路径方式
    front_of_house::hosting::add_to_waitlist();
}
}

清单 7-3:使用绝对与相对路径,调用 add_to_waitlist 函数

第一次调用 eat_at_restaurant 中的 add_to_waitlist 函数时,我们使用的是绝对路径。add_too_waitlist 函数与 eat_at_restaurant,定义在同一个代码箱中,这意味着我们可以使用 crate 关键字,来开始绝对路径。然后,我们逐个包含后续模组,直到找到 add_to_waitlist。咱们可以想象某种具有相同结构的文件系统:我们指定 /front_of_house/hosting/add_to_waitlist 路径,来运行 add_to_waitlist 程序;使用 crate 这个名字,从代码箱根目录开始,就像在 shell 中,使用 / 从文件系统根目录开始一样。

第二次在 eat_at_restaurant 中调用 add_too_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_too_waitlist 的绝对路径,但相对路径仍然有效。但是,如果我们将 eat_at_restaurant 函数单独移到名为 dining 的模组中,那么 add_too_waitlist 调用的绝对路径将保持不变,但相对路径则需要更新。一般来说,我们更倾向于指定绝对路径,因为我们更有可能希望,独立地移动项目的代码定义和项目的调用。

我们来试着编译清单 7-3,看看他为什么还不能编译!我们得到的错误信息,如清单 7-4 所示。

$ cargo build
   Compiling restuarant v0.1.0 (/home/hector/restuarant)
error[E0603]: module `hosting` is private
 --> src/lib.rs:8:28
  |
8 |     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:10:21
   |
10 |     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 `restuarant` (lib) due to 2 previous errors

清单 7-4:构建清单 7-3 中代码时的编译器报错

错误消息表明,模组 hosting 是私有的。换句话说,我们有了 hosting 模组和 add_too_waitlist 函数的正确路径,但 Rust 不允许我们使用他们,因为其无法访问私有部分。在 Rust 中,所有项目(函数、方法、结构体、枚举、模组和常量),默认都是父模组私有的。如果咱们打算将函数或结构体等项目私有化,可以将其放入模组中。

父模组中的项目,不能使用子模组中的私有项目,但子模组中的项目,却可以使用其祖辈模组中的项目。这是因为子模组封装并隐藏了他们的实现细节,但子模组可以看到定义他们的上下文。继续我们的比喻,请把隐私规则,想象成某家餐厅的后台办公室:里面发生的事情,对餐厅顾客来说是隐私,但办公室经理,却可以看到执行做他们所经营餐厅里的一切事情。

Rust 选择让模组系统以这种方式运行,以便在默认情况下,隐藏内部实现细节。这样,咱们就明白,在不破坏外部代码的情况下,可以修改内部代码的哪些部分。不过,Rust 确实提供了选项,让咱们可以通过使用 pub 关键字,将子模组的内部代码,公开给外部的祖辈模组。

使用 pub 关键字暴露路径

Exposing Paths with the pub Keyword

我们回到清单 7-4 中的报错,该报错告诉我们,hosting 模组是私有的。我们希望父模组中的 eat_at_restaurant 函数,能访问子模组中的 add_too_waitlist 函数,因此我们在 hosting 模组中,标记了 pub 关键字,如清单 7-5 所示。

文件名:src/lib.rs

#![allow(unused)]
fn main() {
mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();

    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}
}

清单 7-5:将 hosting 模组声明为 pub,以便在 eat_at_restaurant 中使用他

不幸的是,如下清单 7-6 所示,清单 7-5 中的代码仍会导致报错。

$ cargo build
   Compiling restaurant v0.1.0 (/home/peng/rust-lang/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` due to 2 previous errors

清单 7-6:构建清单 7-5 中代码时出现的编译器报错

发生了什么?在 hosting 模组前添加 pub 关键字后,该模组就变成了公共模组。有了这个改动,如果我们能访问 front_of_house,也就能访问 hosting。但是,hosting内容 仍然是私有的;将该模组构造为公开,并不会使其内容公开。模组上的 pub 关键字,只能让其先辈模组中的代码引用他,而不能访问其内部代码。因为模组是个容器,所以只将模组公开并不能做什么;我们需要更进一步,选择将其模组中的一或多个项目也公开。

清单 7-6 中的报错表明,add_too_waitlist 函数是私有的。隐私规则适用于结构体、枚举、函数和方法以及模组。

我们还可以在 add_too_waitlist 函数的定义前,添加 pub 关键字,使其成为公共函数,如清单 7-7 所示。

文件名:src/lib.rs

#![allow(unused)]
fn main() {
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();

    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}
}

清单 7-7:在 mod hostingfn add_too_waitlist 中添加 pub 关键字后,我们就可以在 eat_at_restaurant 中调用了这个函数

现在代码可以编译了!要了解为何添加 pub 关键字后,我们就可以在 add_too_waitlist 中,在遵守隐私规则下使用这些路径,我们来看看其中的绝对路径和相对路径。

在绝对路径中,我们从 crate 开始,他是咱们代码箱模组树的根。front_of_house 模组就定义在代码箱根中。虽然 front_of_house 不是公有的,但由于 eat_at_restaurant 函数与 front_of_house 模组定义在同一个模组中(也就是说,eat_at_restaurantfront_of_house 属于姊妹关系),我们可以在 eat_at_restaurant 中引用 front_of_house。接下来是标有 pubhosting 模组。我们可以访问 hosting 的父模组,因此可以访问 hosting。最后,add_to_waitlist 函数被标记为 pub,我们可以访问他的父模组,因此这个函数调用是有效的!

在那个相对路径中,除了第一步外,其中的逻辑与绝对路径相同:与从代码箱根开始不同,该相对路径是从 front_of_house 处开始的。这个 front_of_house 模组,是定义在与 eat_at_restaurant 函数同样的模组中,那么从 eat_at_restaurant 定义所在处的这个模组开始的相对路径,就是有效的。随后由于 hostingadd_to_waitlist 都是以 pub 关键字标记过,那么该路径其余部分就都工作了,同时此函数调用就是有效的了!

在计划分享库代码箱,进而其他项目可使用到其代码时,公开 API 即是与该代码箱用户的合约,定下了与库代码箱代码互动的方式。在管理对公共 API 的修改方面,则有着诸多考量,以让人们更易于依赖到咱们的代码箱。这些考量超出了本书的范围;若对这方面感兴趣,那么请参阅 Rust API 指南

带有一个二进制与一个库的 Rust 代码包最佳实践(Best Practice for Packages with a Binary and a Library)

前面提到过 Rust 包可以同时包含一个 src/main.rs 二进制代码箱根,与一个 src/lib.rs 库代码箱根,且这两个代码箱都将默认有着该 Rust 包的名字。一般来说,这种同时包含了一个库及二进制代码箱模式下的包,都会在二进制代码箱中,仅有着足够启动一个会调用到库代码箱代码的可执行程序的少量代码。由于库代码箱的代码可被共享,因此这就实现了别的项目,受益于该 Rust 包所提供的绝大部分功能。

模组树应定义在 src/lib.rs 中。随后,全部的公开程序项目,都可通过以该包名字开头的路径,在那个二进制代码箱中被使用。这个二进制代码箱,就像是个将用到那个库代码箱的完整外部箱,成了库代码箱的一名用户:他只能使用公共 API。这样做有助于设计出良好的 API;你不仅是库代码箱的作者,还是一名库代码箱的客户了!

第 12 章,将以一个会同时包含二进制代码箱与库代码箱的命令行程序,对这种代码组织方式实践加以演示。

使用 super 关键字开始相对路径

Starting Relative Paths with super

通过在路径开头使用 super 关键字,就可以构建出在父模组处,而非当前模组或代码箱根处开始的相对路径。这与以 .. 语法开始的文件系统路径相似。使用 super 实现了对已知在父模组中某个程序项目的引用,在模组与其父模组密切相关,但该父模组在某个时候可能会被迁移到模组树中别的地方时,这种使用 super 关键字的相对路径,就能让模组树的重新安排更为容易。

设想下面清单 7-8 中,建模了一位大厨修正某个不正确点餐,并亲自将其交给顾客的代码。其中定义在 back_of_house 模组中的函数 fix_incorrect_order,通过以 super 作为开头指明的 deliver_order 路径,调用了定义在父模组中的该 deliver_order 函数:

文件名:src/lib.rs

#![allow(unused)]
fn main() {
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 函数,设想作可能维持这同样关系,并在今后决定要对这个代码箱的模组树,进行重新组织时,他们会一起被移动。因此,这里使用了 super,从而今后在此代码被移入到别的模组时,要更新代码的地方就会少一些。

将结构体与枚举构造为公共项目

Making Structs and Enums Public

这里还可以使用 pub 关键字,来将结构体与枚举,指定为公开项目,但结构体与枚举下 pub 的用法,有着几个额外情况。在结构体定义前使用 pub 关键字时,就将该结构体构造为了公开,但该结构体的那些字段,仍将是私有。可根据具体情况,把各个字段构造为公开或不公开。在下面清单 7-9 中,就定义了有着公开 toast 字段,和私有 seasonal_fruit 字段的一个公开的 back_of_house::Breakfast 结构体。这就对在某个饭馆中,顾客可在何处挑选与正餐搭配的面包类型,而主厨则会根据当季及仓库里有些什么,而决定由哪些水果来搭配正餐,这种情形进行了建模。可用的水果变化很快,因此顾客就无法对水果进行选择,甚至他们看不到会得到什么样的水果。

文件名:src/lib.rs

#![allow(unused)]
fn main() {
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() {
    // 点下一份带有黑麦土司的夏日早餐, rye, US /raɪ/, UK /rai/, n.黑麦, 黑麦粒
    let mut meal = back_of_house::Breakfast::summer("Rye");
    meal.toast = String::from("Wheat");
    println! ("请给我一份 {} 土司", meal.toast);

    // 若不把接下来的行注释掉,那么就不会编译;这里不允许查看或修改
    // 餐食搭配的应季水果
    // 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 字段值进行修改的行解除注释,看看将得到什么样的错误!

$ cargo build
   Compiling restaurant v0.1.0 (/home/peng/rust-lang/restaurant)
error[E0616]: field `seasonal_fruit` of struct `Breakfast` is private
  --> src/lib.rs:25:10
   |
25 |     meal.seasonal_fruit = String::from("blueberries");
   |          ^^^^^^^^^^^^^^ private field

For more information about this error, try `rustc --explain E0616`.
error: could not compile `restaurant` due to previous error

还请留意由于 back_of_restaurant::Breakfast 有个私有字段,那么该结构体就需要提供一个公开的、构造出Breakfast 实例的关联函数(这里将该函数命名为了 summer)。若 Breakfast 没有这样一个函数,那么由于在 eat_at_restaurant 中无法设置那个私有 seasonal_fruit 字段的值,因此就没法在 eat_at_restaurant 中创建处一个 Breakfast 的实例来。

与此相比,在将枚举构造为公开时,该枚举的全部变种此时都是公开的。这里就只需在 enum 关键字前的 pub 关键字,如下清单 7-10 中所示。

文件名:src/lib.rs

#![allow(unused)]
fn main() {
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;
}
}

appetizer, US/ˈæpəˌtaɪzər/, UK/ˈæpəˌtaɪzə(r)/ n.(餐前的)开胃品

清单 7-10:将枚举指定为公开,则会将其全部变种构造为公开

由于这里将那个 Appetizer 枚举构造为了公开,因此就可以在 eat_at_restaurant 中使用 SoupSalad 变种。除非枚举的各个变种是公开的,否则枚举就不是非常有用了;若在所有场合,都必须以 pub 关键字来对全部枚举变种进行注解,那就会让人觉得烦恼不已,因此默认枚举变种就是公开的。而结构体则通常无需其字段保持公开就有用处,因此结构体的那些字段,就遵循了除非以 pub 关键字注释,而默认全部为私有的一般规则。

还有一个尚未讲到的涉及 pub 关键字的情况,那也是最后的一项模组系统特性:use 关键字。后面会先讲 use 本身,然后再给出怎样结合 pubuse

Last change: 2024-01-17, commit: 2265fb7