Rust 模块树中的路径

RustRustBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

欢迎来到模块树中引用项的路径。本实验是《Rust 程序设计语言》的一部分。你可以在 LabEx 中练习 Rust 技能。

在本实验中,我们将学习 Rust 中用于引用模块树中项的路径,它可以采用绝对路径或相对路径的形式。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/MemorySafetyandManagementGroup(["Memory Safety and Management"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/MemorySafetyandManagementGroup -.-> rust/lifetime_specifiers("Lifetime Specifiers") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") subgraph Lab Skills rust/variable_declarations -.-> lab-100403{{"Rust 模块树中的路径"}} rust/mutable_variables -.-> lab-100403{{"Rust 模块树中的路径"}} rust/string_type -.-> lab-100403{{"Rust 模块树中的路径"}} rust/function_syntax -.-> lab-100403{{"Rust 模块树中的路径"}} rust/expressions_statements -.-> lab-100403{{"Rust 模块树中的路径"}} rust/lifetime_specifiers -.-> lab-100403{{"Rust 模块树中的路径"}} rust/method_syntax -.-> lab-100403{{"Rust 模块树中的路径"}} rust/traits -.-> lab-100403{{"Rust 模块树中的路径"}} end

模块树中引用项的路径

为了向 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() {
    // 绝对路径
    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_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();
  |                            ^^^^^^^ 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();
   |                     ^^^^^^^ private module
   |
note: the module `hosting` is defined here
  --> src/lib.rs:2:5
   |
2  |     mod hosting {
   |     ^^^^^^^^^^^

清单 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() {}
    }
}

--snip--

清单 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() {}
   |         ^^^^^^^^^^^^^^^^^^^^

清单 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--

清单 7-7:在 mod hostingfn add_to_waitlist 上添加 pub 关键字后,我们就可以从 eat_at_restaurant 中调用该函数了。

现在代码将编译通过!为了理解为什么添加 pub 关键字能让我们在 add_to_waitlist 中使用这些路径,让我们来看一下绝对路径和相对路径。

在绝对路径中,我们从 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。接下来是用 pub 标记的 hosting 模块。我们可以访问 hosting 的父模块,所以我们可以访问 hosting。最后,add_to_waitlist 函数用 pub 标记,我们可以访问它的父模块,所以这个函数调用是可行的!

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

如果你打算共享你的库 crate,以便其他项目可以使用你的代码,那么你的公共 API 就是你与 crate 用户的契约,它决定了他们如何与你的代码进行交互。围绕管理公共 API 的更改有很多需要考虑的因素,以便人们更容易依赖你的 crate。这些考虑超出了本书的范围;如果你对这个主题感兴趣,请查看 Rust API 指南:https://rust-lang.github.io/api-guidelines

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

我们提到过一个包可以同时包含 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 函数通过指定从 super 开始的 deliver_order 路径来调用父模块中定义的 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 并找到了它。成功!我们认为 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() {
    // 点一份夏季的黑麦面包早餐
    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 字段。注意,在 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 结合起来。

总结

恭喜你!你已经完成了“模块树中引用项的路径”实验。你可以在 LabEx 中练习更多实验来提升你的技能。