键盘快捷键

按 ← 或 → 在章节间切换

按 S 或 / 搜索本书

按 ? 显示此帮助

按 Esc 隐藏此帮助

引用模块树中项目的路径

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

路径有两种形式:

  • 绝对路径是从 crate 根开始的完整路径;引用外部 crate 中的代码时,绝对路径以 crate 名称开头;引用当前 crate 中的代码时,则以字面值 crate 开头。
  • 相对路径从当前模块开始,并使用 self、super 或当前模块中的标识符。

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

回到示例 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。你可以想象一个结构相同的文件系统:若要运行 add_to_waitlist 程序,就指定路径 /front_of_house/hosting/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
示例 7-4:构建示例 7-3 中代码时的编译器错误

错误消息指出 hosting 模块是私有的。换句话说,hosting 模块和 add_to_waitlist 函数的路径是正确的,但 Rust 不允许我们使用它们,因为当前代码无法访问私有部分。在 Rust 中,所有项目(函数、方法、结构体、枚举、模块和常量)默认都对父模块私有。如果想让函数或结构体等项目保持私有,就将它放在模块中。

父模块中的项目不能使用子模块中的私有项目,但子模块中的项目可以使用其祖先模块中的项目。这是因为子模块会封装并隐藏自己的实现细节,但它们可以看到定义它们的上下文。继续使用前面的比喻,可以把隐私规则想象成餐厅的后勤办公室:餐厅顾客不能知道办公室内发生的事情,但办公室经理可以查看并管理其负责餐厅中的一切。

Rust 选择让模块系统以这种方式工作,使隐藏内部实现细节成为默认行为。这样,你就知道可以修改哪些内部代码而不会破坏外部代码。不过,Rust 允许你使用 pub 关键字公开项目,从而让外层祖先模块访问子模块代码中的内部项目。

使用 pub 关键字公开路径

回到示例 7-4 中指出 hosting 模块是私有的错误。我们希望父模块中的 eat_at_restaurant 函数能够访问子模块中的 add_to_waitlist 函数,因此在 hosting 模块前添加 pub 关键字,如示例 7-5 所示。

文件名:src/lib.rs
mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

// -- snip --
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:10:37
   |
10 |     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:13:30
   |
13 |     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 关键字,将其设为公开,如示例 7-7 所示。

文件名:src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// -- snip --
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:在 mod hosting 和 fn add_to_waitlist 前添加 pub 关键字后,就可以从 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。接下来是标记为 pub 的 hosting 模块。我们可以访问 hosting 的父模块,因此也可以访问 hosting。最后,add_to_waitlist 函数也标记为 pub,而我们可以访问它的父模块,因此该函数调用有效!

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

如果你计划分享自己的库 crate,让其他项目使用其中的代码,那么公共 API 就是你与 crate 用户之间的契约,它决定了用户如何与你的代码交互。管理公共 API 的变更涉及许多考量,以便其他人更容易依赖你的 crate。这些内容超出了本书的范围;如果你对此感兴趣,可以参阅Rust API 指南。

同时包含二进制 crate 和库 crate 的包的最佳实践

前面提到,一个包可以同时包含以 src/main.rs 为 crate 根的二进制 crate 和以 src/lib.rs 为 crate 根的库 crate,并且默认情况下二者都与包同名。通常,同时包含库 crate 和二进制 crate 的包只会在二进制 crate 中放入足够的代码,用来启动可执行程序并调用库 crate 中定义的代码。这样,其他项目就能从该包提供的大部分功能中受益,因为库 crate 中的代码可以共享。

模块树应定义在 src/lib.rs 中。之后,二进制 crate 可以通过以包名开头的路径使用其中的公开项目。二进制 crate 使用库 crate 的方式与完全外部的 crate 相同:它只能使用公共 API。这有助于你设计良好的 API;你不仅是作者,也是使用者!

第 12 章将通过一个同时包含二进制 crate 和库 crate 的命令行程序,演示这种组织方式。

使用 super 开始相对路径

我们可以在路径开头使用 super,让相对路径从父模块开始,而不是从当前模块或 crate 根开始。这类似于在文件系统路径开头使用 ..,表示转到父目录。使用 super 可以引用我们确定位于父模块中的项目;如果某个模块与父模块关系密切,但父模块将来可能在模块树中移到其他位置,这样做能让调整模块树更容易。

考虑示例 7-8 中的代码,它模拟了厨师修正错误订单并亲自将订单送给顾客的情形。back_of_house 模块中定义的 fix_incorrect_order 函数,通过以 super 开头的路径调用父模块中定义的 deliver_order 函数。

文件名: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。成功了!我们认为,如果以后决定重新组织 crate 的模块树,back_of_house 模块和 deliver_order 函数很可能会保持彼此之间的关系并一起移动。因此,我们使用 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 字段值那一行的注释,看看会得到什么错误!

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

相反,如果我们将枚举设为公开,它的所有变体也都会公开。如示例 7-10 所示,只需在 enum 关键字前添加 pub 即可。

文件名: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 中使用 Soup 和 Salad 变体。

如果枚举变体不公开,枚举就不太有用;若每次都必须为所有枚举变体添加 pub 注解,会很麻烦,因此枚举变体默认是公开的。结构体即使字段不公开通常也很有用,所以结构体字段遵循一般规则:除非使用 pub 标注,否则默认都是私有的。

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