模块树中引用项的路径

为了告诉 Rust 在模块树中查找某个项的位置,我们使用路径,就像在导航文件系统时使用路径一样。要调用一个函数,我们需要知道它的路径。

路径可以有两种形式:

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

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

回到清单 7-1,假设我们想要调用 add_to_waitlist 函数。 这相当于问:add_to_waitlist 函数的路径是什么? 清单 7-3 包含了清单 7-1 的部分内容,其中一些模块和函数已被移除。

我们将展示两种从新函数eat_at_restaurant调用add_to_waitlist函数的方法,这些函数在crate根目录中定义。这些路径是正确的,但还有一个问题会导致此示例无法编译。我们稍后会解释原因。

eat_at_restaurant 函数是我们库crate的公共API的一部分,所以我们用 pub 关键字标记它。在 “使用 pub 关键字暴露路径” 部分,我们将详细介绍 pub

Filename: 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();
}
Listing 7-3: Calling the add_to_waitlist function using absolute and relative paths

第一次在 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 开始,这是与 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
Listing 7-4: Compiler errors from building the code in Listing 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 所示。

Filename: 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();
}
Listing 7-5: Declaring the hosting module as pub to use it from 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
Listing 7-6: Compiler errors from building the code in Listing 7-5

发生了什么?在 mod hosting 前面添加 pub 关键字使模块变为公共模块。通过这一更改,如果我们能够访问 front_of_house,我们就可以访问 hosting。但是 hosting内容 仍然是私有的;将模块设为公共模块并不会使其内容也变为公共的。pub 关键字在模块上仅允许其祖先模块中的代码引用它,而不能访问其内部代码。由于模块是容器,仅将模块设为公共模块并不能做太多事情;我们需要进一步选择将模块内的一个或多个项设为公共的。

列表 7-6 中的错误表明 add_to_waitlist 函数是私有的。 隐私规则适用于结构体、枚举、函数和方法以及模块。

让我们也通过在定义前添加 pub 关键字来使 add_to_waitlist 函数公开,如清单 7-7 所示。

Filename: 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();
}
Listing 7-7: Adding the pub keyword to mod hosting and fn add_to_waitlist lets us call the function from 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。接下来是标记为 pubhosting 模块。我们可以访问 hosting 的父模块,因此我们可以访问 hosting。最后, add_to_waitlist 函数标记为 pub,并且我们可以访问其父模块,因此这个函数调用可以工作!

在相对路径中,逻辑与绝对路径相同,只是第一步不同:不是从crate根开始,而是从front_of_house开始。因为front_of_house模块是在与eat_at_restaurant相同的模块中定义的,所以从定义eat_at_restaurant的模块开始的相对路径是有效的。然后,由于hostingadd_to_waitlist被标记为pub,路径的其余部分也有效,因此这个函数调用是有效的!

如果您计划共享您的库crate,以便其他项目可以使用您的代码,您的公共API就是您与crate用户的契约,决定了他们如何与您的代码交互。在管理公共API的变更时有许多需要考虑的因素,以使人们更容易依赖您的crate。这些考虑超出了本书的范围;如果您对这个主题感兴趣,请参见Rust API Guidelines

Best Practices for Packages with a Binary and a Library

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

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

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

super 开始的相对路径

我们可以使用 super 在路径的开头构造从父模块而不是当前模块或 crate 根开始的相对路径。这类似于使用 .. 语法开始文件系统路径。使用 super 可以让我们引用我们知道在父模块中的项,这可以在模块与父模块紧密相关但父模块将来可能会在模块树中移动到其他位置时,使重新排列模块树更加容易。

考虑清单 7-8 中的代码,该代码模拟了一位厨师修正错误订单并亲自将其送到客户手中的情况。back_of_house 模块中定义的 fix_incorrect_order 函数通过指定从 super 开始的 deliver_order 路径,调用了父模块中定义的 deliver_order 函数。

Filename: src/lib.rs
fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order();
    }

    fn cook_order() {}
}
Listing 7-8: Calling a function using a relative path starting with 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 字段是私有的。这模拟了在餐厅中顾客可以选择随餐提供的面包类型,但厨师根据季节和库存决定随餐提供的水果。可用的水果变化很快,因此顾客不能选择水果,甚至不能看到他们会得到哪种水果。

Filename: 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");
}
Listing 7-9: A struct with some public fields and some private fields

因为 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 所示。

Filename: 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;
}
Listing 7-10: Designating an enum as public makes all its variants public

因为我们将 Appetizer 枚举公开,所以可以在 eat_at_restaurant 中使用 SoupSalad 变体。

枚举类型除非其变体是公开的,否则并不是很实用;如果每次都要为所有枚举变体标注pub会很麻烦,所以枚举变体的默认状态是公开的。结构体即使其字段不是公开的也经常是有用的,因此结构体字段遵循一切默认为私有的规则,除非标注了pub

还有另一种涉及pub的情况我们尚未讨论,那就是我们模块系统中的最后一个特性:use关键字。我们首先单独介绍use,然后展示如何结合使用pubuse