rust-模块树中引用项的路径

模块树中引用项的路径

为了告诉 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

rust 复制代码
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 函数,因此我们在 hosting 模块前加上 pub 关键字,如列表 7-5 所示。

文件名:src/lib.rs

rust 复制代码
mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

// -- snip --

清单7-5:将宿主模块声明为pub以便从eat_at_restaurant中使用

不幸的是,清单7-5中的代码仍然会导致编译错误,如清单7-6所示。

rust 复制代码
$ 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 函数是私有的。隐私规则适用于结构体、枚举、函数和方法以及模块。

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

文件名:src/lib.rs

复制代码
mod front_of_house {  
    pub mod hosting {  
        pub fn 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 就是你与用户之间确定如何交互代码的契约。在管理公共 API 更改方面,有许多考虑因素,以便他人更容易依赖你的 crate。这些内容超出本书范围;如果感兴趣,请参阅《Rust API 指南》

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

之前提到,一个包既可以包含 src/main.rs(二进制 crate 根),又可以包含 src/lib.rs(库 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 路径调用了定义在父模块中的 deliver_order 函数。

文件名:src/lib.rs

rust 复制代码
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

rust 复制代码
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() {
    // 夏天点一份带黑麦吐司(Rye)的早餐。
    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 中可以通过点号访问读写该字段。但不能访问 seasonal_fruit,因为它是私有字段。尝试取消注释修改 seasonal_fruit 那行代码,会看到编译错误!

另外,由于 BackOfHouse 有私有字段,该结构必须提供一个公共关联函数用于创建实例(此处命名为 summer)。否则,在 eat_at_restaurant 无法创建 Breakfast 实例,因为无法设置 private 字段值。

相比之下,如果将枚举设为 public,那么所有变体都是 public,只需在 enum 前加 pub,如清单 7-10 所示:

文件名:src/lib.rs

rust 复制代码
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:将枚举声明为 public 会使所有变体都成为 public

因为 Appetizer 枚举被声明为 public,所以在 eat_at_restaurant 中能够使用 Soup 和 Salad 两个变体。

除非其变体也是公有,否则枚举没什么用;每次都给所有变体现加上 pub 很麻烦,因此默认情况下,enum 的所有变体现均为公有。而 struct 通常即使没有公开其字段也很实用,所以 struct 字段默认全部私有,除非显式标记为 pub。

还有一种涉及 pub 的情况尚未介绍,那就是最后一个关于模组系统特性的 use 关键字。接下来先讲解 use 本身,然后再展示如何结合使用 pub 和 use 。

相关推荐
阿虎儿34 分钟前
React Context 详解:从入门到性能优化
前端·vue.js·react.js
Ray Liang1 小时前
用六边形架构与整洁架构对比是伪命题?
java·python·c#·架构设计
Sailing1 小时前
🚀 别再乱写 16px 了!CSS 单位体系已经进入“计算时代”,真正的响应式布局
前端·css·面试
Java水解1 小时前
Java 中间件:Dubbo 服务降级(Mock 机制)
java·后端
喝水的长颈鹿1 小时前
【大白话前端 03】Web 标准与最佳实践
前端
爱泡脚的鸡腿1 小时前
Node.js 拓展
前端·后端
蚂蚁背大象2 小时前
Rust 所有权系统是为了解决什么问题
后端·rust
左夕2 小时前
分不清apply,bind,call?看这篇文章就够了
前端·javascript
布列瑟农的星空3 小时前
前端都能看懂的rust入门教程(五)—— 所有权
rust
Zha0Zhun3 小时前
一个使用ViewBinding封装的Dialog
前端