写点什么

Rust 从 0 到 1- 代码组织 - 路径

用户头像
关注
发布于: 2021 年 04 月 27 日
Rust从0到1-代码组织-路径

下面我们来看一下如何在 Rust 模块树中找到我们需要引用的函数、结构体或枚举等。就像在文件系统使用路径表示一个文件的具体位置一样,我们使用路径(Paths)来表示我们引用的内容的具体位置,譬如,如果我们想要调用一个函数,我们就需要知道它的路径。

路径有两种形式:

  • 绝对路径(absolute path)从 crate root 开始,以 crate 的名字或者 crate 开头。

  • 相对路径(relative path)从当前模块开始,以 selfsuper 或当前模块的名字开头。

绝对路径和相对路径都是由一个或多个由 :: 分割的标识符组成(就像文件系统的  / 路径分隔符)。

继续使用前面餐馆的例子,我们如何调用 add_to_waitlist 函数?它的路径是什么?下面我们通过添加一个新函数 eat_at_restaurant 来展示调用 add_to_waitlist 函数的方法。eat_at_restaurant 函数是餐馆 lib crate 的一个公开 API,我们使用 pub 关键字来标记它(接下来我们会详细介绍 pub,并且这个例子无法编译通过,原因稍后解释):

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();}
复制代码

上面的例子是在同一个 crate 种调用的方式(后面我们会将介绍在不同 crate 中如何调用),我们在 eat_at_restaurant 中使用绝对路径和相对路径两种方式调用了 add_to_waitlist 函数。在使用绝对路径方式时,我们在 crate 后面持续的追加模块,直到我们找到 add_to_waitlist。类似在文件系统,我们通过指定路径 /front_of_house/hosting/add_to_waitlist 来执行 add_to_waitlist 程序(就像在 shell 中使用 / 从文件系统根开始)。在使用相对路径时,由于在模块树中,front_of_house  与 eat_at_restaurant 定义在同一层级,因此我们从 front_of_house 开始寻找 add_to_waitlist,与之类似的文件系统路径就是 front_of_house/hosting/add_to_waitlist。

使用相对路径还是绝对路径,主要还是要取决于具体的场景,取决于两部分代码的相对独立性。举例来说,假设我们要将 front_of_house 模块和 eat_at_restaurant 函数一起移动 customer_experience 模块中,如果使用绝对路径,那么 add_to_waitlist 的引用路径就需要修改,而相对路径则不需要。反之,如果我们要将 eat_at_restaurant 函数单独移到 customer_experience 模块中,如果使用绝对路径,那么 add_to_waitlist 的引用路径就不需要修改,但是相对路径则需要。

Rust 中模块对代码的组织还包括代码的私有性(privacy boundary):私有代码将不允许外部代码知道、调用和依赖被封装的实现细节。因此,我们可以将,譬如函数或结构体放入模块中来获得私有性。

在 Rust 中函数、方法、结构体、枚举、模块和常量等默认都是私有的。父模块中不能使用子模块中的私有项,但是子模块可以使用他们父模块中的私有项。这么做是考虑父模块是子模块的上下文,子模块封装并隐藏了自己的实现详情,但是子模块应该可以看到他们所处的上下文(我理解私有性是对外部来说的,不是对内部的)。继续以餐馆作为例子来类私有性规则:餐馆内的后台办公室的情况对餐厅顾客来说是不可知的,但办公室经理可以洞悉其经营的餐厅情况并发布指令。

总之,Rust 选择默认隐藏内部实现细节。这样一来,我们就知道可以放心的去更改内部的哪些部分代码而不会影响外部代码调用。当然,我们还可以通过使用 pub 关键字来创建公开部分,使模块的一部分暴露给外部。因此,上面的例子是无法编译通过的。因为虽然路径是正确的,但是 hosting 模块和 add_to_waitlist 函数是私有的,无法访问。

使用 pub 关键字

我们对前面的例子进行修改让父模块中的 eat_at_restaurant 函数可以访问子模块中的 add_to_waitlist 函数,因此我们给 hosting 模块加上 pub 关键字:

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();}
复制代码

不过,编译仍然会报错,为什么呢?我们已经在 mod hosting 前添加了 pub 关键字,使其变成公开的。这是由于 hosting 的内容仍然是私有的;声明模块是公开的并不代表其包含的内容也是公开的。模块上的 pub 关键字只是允许其父模块可以引用它了,但是 add_to_waitlist 函数仍然是私有的。私有性规则不但应用于模块,还应用于结构体、枚举、函数和方法。下面我们继续给 add_to_waitlist 函数添加 pub 关键字:

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();}
复制代码

现在代码可以编译通过了!根据私有性规则,我们从 crate,也就是 crate root 开始看一遍。crate root 中定义了 front_of_house 模块。front_of_house 模块并没有声明为公开的,但是因为 eat_at_restaurant 函数与 front_of_house 处于同一模块中(即兄弟关系),我们可以在 eat_at_restaurant 中引用 front_of_house。接下来是使用 pub 关键字定义的 hosting 模块。我们可以访问 hosting 的父模块,并且 hosting 是公开的,所以可以访问 hosting。最后,add_to_waitlist 函数也被定义为 pub ,并且他的父模块(hosting)也是可以访问的,所以这个函数可以被调用!相对路径的访问,其逻辑与绝对路径相同,除了它是从 front_of_house 开始而不是从 crate root 开始。

使用 super

我们还可以使用 super 来访问相对路径,类似于文件系统中以 .. 访问父目录。那么为什么要这样做呢?下面我们举例来说明,参考下面的代码,它模拟了后厨更正了一个错误订单,并亲自将其提供给客户。fix_incorrect_order 函数通过 super 起始的相对路径,来调用 server_order 函数:

fn serve_order() {}
mod back_of_house { fn fix_incorrect_order() { cook_order(); super::serve_order(); }
fn cook_order() {}}
复制代码

fix_incorrect_order 函数包含在 back_of_house 模块中,因此我们可以使用 super 访问 back_of_house 的父模块,在例子中就是 crate root,并找到 serve_order 调用它(子模块可以访问父模块)。假设我们认为 back_of_house 模块和 server_order 函数之间关联性较强,如果我们要重构这个 crate 的模块树,需要一起移动它们。因此,我们使用 super,这样一来,如果这些代码被移动到了其他模块,我们只需要更新很少的代码。

公有结构体和枚举

我们还可以使用 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 是私有的,否则编译器会报错!

另外需要注意的是,因为 back_of_house :: Breakfast 包含私有字段,所以需要提供一个公共的关联函数(这个在前面章节讲过)来构造 Breakfast (例子中是 summer)。否则,我们将无法在 eat_at_restaurant 中创建 Breakfast 实例,因为我们无法设置私有字段 seasonal_fruit 的值。

而枚举与结构体不同,如果我们将枚举设为公有的,则它的所有成员都是公有的:

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 中使用成员 Soup 和 Salad 。这么设计主要是考虑枚举成员通常就是给外部使用的,而要给每个枚举成员都添加 pub 比较繁琐,因此枚举成员默认就是公有的。

另外,还有一种使用 pub 的场景将在下一章介绍,即 use 关键字。

发布于: 2021 年 04 月 27 日阅读数: 9
用户头像

关注

公众号"山 顽石"欢迎大家关注;) 2021.03.23 加入

IT老兵,关注Rust、Java、前端、架构、大数据、AI、算法等;平时喜欢美食、旅游、宠物、健身、篮球、乒乓球等。希望能和大家交流分享。

评论

发布
暂无评论
Rust从0到1-代码组织-路径