【Rust学习】07_3_使用路径引用模块树中的项目

前言

来看一下 Rust 如何在模块树中找到一个项目的位置,我们使用路径的方式,就像在文件系统使用路径一样。如果我们想要调用一个函数,我们需要知道它的路径。

路径可以采用两种形式:

  • 绝对路径absolute path)从 crate 根部开始,以 crate 名或者字面量 crate 开头。
  • 相对路径relative path)从当前模块开始,以 selfsuper 或当前模块的标识符开头。

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

内容

回到前面的餐厅的示例,假设我们想调用 add_to_waitlist 函数。这与问add_to_waitlist 函数的路径是什么相同?现在我们简化下这个代码,删除其中的一些模块和函数。我们将展示两种从 crate 根中定义的新函数 eat_at_restaurant 调用 add_to_waitlist 函数的方法。这些路径是正确的,但仍然存在另一个问题,这将阻止代码的编译。我们稍后会解释原因。eat_at_restaurant 函数是我们库 crate 的公共 API 的一部分,因此我们使用 pub 关键字来标记它。

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

fn main() {
    eat_at_restaurant();
}

我们第一次在 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,但是相对路径必须要更新。我们更倾向于使用绝对路径,因为把代码定义和项调用各自独立地移动是更常见的。

让我们尝试编译示例中的代码并找出为什么它还不能编译!我们得到的以下错误:

$ cargo build                                                                              ✔  15:10:16  
  Compiling restaurant v0.1.0 (/Users/wangyang/Documents/project/rust-learn/restaurant)
error[E0603]: module `hosting` is private
  --> src/lib.rs:26:28
   |
26 |     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:19:6
   |
19 |      mod hosting {
   |      ^^^^^^^^^^^

error[E0603]: module `hosting` is private
  --> src/lib.rs:29:21
   |
29 |     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:19:6
   |
19 |      mod hosting {
   |      ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors

错误信息说 hosting 模块是私有的。换句话说,我们拥有 hosting 模块和 add_to_waitlist 函数的的正确路径,但是 Rust 不让我们使用,因为它不能访问私有代码。在 Rust 中,默认情况下,所有项(函数、方法、结构、枚举、模块和常量)都是父模块私有的。如果你想让函数或结构体之类的项成为私有的,你可以把它放在一个模块中。

父模块中的项不能使用子模块中的私有项,但子模块中的项可以使用其祖先模块中的项。这是因为子模块会包装和隐藏其实现详细信息,但子模块可以看到定义它们的上下文。继续我们的比喻,将隐私规则想象成餐厅的后台办公室:那里发生的事情对餐厅客户来说是私人的,但办公室经理可以看到他们经营的餐厅并做任何事情。

Rust 选择以这种方式实现模块系统功能,因此默认隐藏内部实现细节。这样,您就知道可以在不破坏外部代码的情况下更改内部代码的哪些部分。但是,Rust 确实为您提供了通过使用 pub 关键字将项目公开来将子模块代码的内部部分暴露给外部祖先模块的选项。

使用pub关键字暴露路径

让我们回到前面示例的错误里,它告诉我们 hosting 模块和add_to_waitlist函数是私有的。我们希望父模块中的 eat_at_restaurant 函数能够访问子模块中的 add_to_waitlist 函数,因此我们用 pub 关键字标记 hosting 模块和add_to_waitlist 函数,如下所示:

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();
}

fn main() {
    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,路径其余的部分也是有效的,因此函数调用也是有效的!

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

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

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

使用super 起始的相对路径

我们可以通过在路径的开头使用 super 来构造从父模块开始的相对路径,而不是当前模块或 crate 根。这就像使用 .. 语法启动文件系统路径一样。使用 super 可以让我们引用我们知道在父模块中的项,当模块与父模块密切相关但有一天父模块可能会移动到模块树中的其他位置时,这可以使重新排列模块树变得更容易。

它模拟了厨师修复错误订单并亲自将其带给客户的情况。back_of_house 模块中定义的函数 fix_incorrect_order 通过指定deliver_order路径(以 super 开头)来调用父模块中定义的函数 deliver_order

fn deliver_order() {}

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

    fn cook_order() {}
}

fix_incorrect_order 函数在 back_of_house 模块中,因此我们可以使用 super 转到 back_of_house 的父模块,在本例中是 crate根。从那里,我们寻找deliver_order并找成功找到它。我们认为 back_of_house 模块和 deliver_order 函数可能会保持相同的关系,如果我们决定重新组织 crate 的模块树,它们可能会一起移动。因此,我们使用了 super,这样如果这些代码被移动到不同的模块,我们只需要更新很少的代码。

创建公有的结构体和枚举

mo我们也可以使用 pub 将结构和枚举指定为公有的,但将 pub 与结构和枚举一起使用还有一些额外的细节。如果我们在结构体定义之前使用 pub,我们会将结构体设为公有的,但结构体的字段仍然是私有的。我们可以根据具体情况将每个字段设为公开或不公开。在下面的示例中,我们定义了一个公有结构体 back_of_house::Breakfast,其中有一个公有字段 toast 和私有字段 seasonal_fruit。这模拟了一家餐厅的情况,顾客可以选择随餐吃的面包类型,但厨师根据季节和库存来决定哪些水果搭配这顿饭。可用的水果变化很快,因此客户无法选择水果,甚至无法看到他们将获得哪种水果。

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");
}

因为 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,如下所示:

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 的公有枚举,所以我们可以在 eat_at_restaurant 中使用 SoupSalad 成员。如果枚举成员不是公有的,那么枚举会显得用处不大;给枚举的所有成员挨个添加 pub 是很令人恼火的,因此枚举成员默认就是公有的。结构体通常使用时,不必将它们的字段公有化,因此结构体遵循常规,内容全部是私有的,除非使用 pub 关键字。

还有一种使用 pub 的场景我们还没有介绍,那就是我们最后要讲的模块功能:use 关键字。我们将先单独介绍 use,然后展示如何结合使用 pubuse

posted @ 2024-11-19 11:05:09 王洋 阅读(111) 评论(2)

  1. #1楼 2024-11-20 07:47 ekegdwxdpa Windows 10.0 Sogou 2.X 中国 浙江省 宁波市 电信


    真好呢

  2. #2楼 2024-11-19 19:02 bmzzyyzmyk Windows 10.0 Sogou 2.X 美国 纽约 纽约


    你的才华让人惊叹,请继续保持。 http://www.55baobei.com/SbeZ5Ex1lE.html

发表评论
昵称
邮箱
网址