模块树中引用项的路径

为了告诉 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 开头,该模块的名称与模块树中 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 函数是私有的。 隐私规则也适用于结构体、枚举、函数和方法以及模块。

让我们也通过在其定义之前添加 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。 接下来是标记为 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 指南

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

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

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

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

使用 super 开始相对路径

我们可以通过在路径的开头使用 super 来构造从父模块(而不是当前模块或 crate 根目录)开始的相对路径。 这类似于使用 .. 语法启动文件系统路径。 使用 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