ch07-03-paths-for-referring-to-an-item-in-the-module-tree.md
commit 9c0fa2714859738ff73cbbb829592e4c037d7e46
來看一下 Rust 如何在模塊樹中找到一個項的位置,我們使用路徑的方式,就像在文件系統(tǒng)使用路徑一樣。如果我們想要調(diào)用一個函數(shù),我們需要知道它的路徑。
路徑有兩種形式:
crate
?開頭。self
?、?super
?或當前模塊的標識符開頭。絕對路徑和相對路徑都后跟一個或多個由雙冒號(::
)分割的標識符。
讓我們回到示例 7-1。我們?nèi)绾握{(diào)用 add_to_waitlist
函數(shù)?還是同樣的問題,add_to_waitlist
函數(shù)的路徑是什么?在示例 7-3 中,我們通過刪除一些模塊和函數(shù),稍微簡化了一下我們的代碼。我們在 crate 根定義了一個新函數(shù) eat_at_restaurant
,并在其中展示調(diào)用 add_to_waitlist
函數(shù)的兩種方法。eat_at_restaurant
函數(shù)是我們 crate 庫的一個公共 API,所以我們使用 pub
關鍵字來標記它。在 “使用pub關鍵字暴露路徑” 一節(jié),我們將詳細介紹 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: 使用絕對路徑和相對路徑來調(diào)用 add_to_waitlist
函數(shù)
第一種方式,我們在 eat_at_restaurant
中調(diào)用 add_to_waitlist
函數(shù),使用的是絕對路徑。add_to_waitlist
函數(shù)與 eat_at_restaurant
被定義在同一 crate 中,這意味著我們可以使用 crate
關鍵字為起始的絕對路徑。
在 crate
后面,我們持續(xù)地嵌入模塊,直到我們找到 add_to_waitlist
。你可以想象出一個相同結構的文件系統(tǒng),我們通過指定路徑 /front_of_house/hosting/add_to_waitlist
來執(zhí)行 add_to_waitlist
程序。我們使用 crate
從 crate 根開始就類似于在 shell 中使用 /
從文件系統(tǒng)根開始。
第二種方式,我們在 eat_at_restaurant
中調(diào)用 add_to_waitlist
,使用的是相對路徑。這個路徑以 front_of_house
為起始,這個模塊在模塊樹中,與 eat_at_restaurant
定義在同一層級。與之等價的文件系統(tǒng)路徑就是 front_of_house/hosting/add_to_waitlist
。以名稱為起始,意味著該路徑是相對路徑。
選擇使用相對路徑還是絕對路徑,還是要取決于你的項目。取決于你是更傾向于將項的定義代碼與使用該項的代碼分開來移動,還是一起移動。舉一個例子,如果我們要將 front_of_house
模塊和 eat_at_restaurant
函數(shù)一起移動到一個名為 customer_experience
的模塊中,我們需要更新 add_to_waitlist
的絕對路徑,但是相對路徑還是可用的。然而,如果我們要將 eat_at_restaurant
函數(shù)單獨移到一個名為 dining
的模塊中,還是可以使用原本的絕對路徑來調(diào)用 add_to_waitlist
,但是相對路徑必須要更新。我們更傾向于使用絕對路徑,因為把代碼定義和項調(diào)用各自獨立地移動是更常見的。
讓我們試著編譯一下示例 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 {
| ^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors
示例 7-4: 構建示例 7-3 出現(xiàn)的編譯器錯誤
錯誤信息說 hosting
模塊是私有的。換句話說,我們擁有 hosting
模塊和 add_to_waitlist
函數(shù)的的正確路徑,但是 Rust 不讓我們使用,因為它不能訪問私有片段。
模塊不僅對于你組織代碼很有用。他們還定義了 Rust 的 私有性邊界(privacy boundary):這條界線不允許外部代碼了解、調(diào)用和依賴被封裝的實現(xiàn)細節(jié)。所以,如果你希望創(chuàng)建一個私有函數(shù)或結構體,你可以將其放入模塊。
Rust 中默認所有項(函數(shù)、方法、結構體、枚舉、模塊和常量)都是私有的。父模塊中的項不能使用子模塊中的私有項,但是子模塊中的項可以使用他們父模塊中的項。這是因為子模塊封裝并隱藏了他們的實現(xiàn)詳情,但是子模塊可以看到他們定義的上下文。繼續(xù)拿餐館作比喻,把私有性規(guī)則想象成餐館的后臺辦公室:餐館內(nèi)的事務對餐廳顧客來說是不可知的,但辦公室經(jīng)理可以洞悉其經(jīng)營的餐廳并在其中做任何事情。
Rust 選擇以這種方式來實現(xiàn)模塊系統(tǒng)功能,因此默認隱藏內(nèi)部實現(xiàn)細節(jié)。這樣一來,你就知道可以更改內(nèi)部代碼的哪些部分而不會破壞外部代碼。你還可以通過使用 pub
關鍵字來創(chuàng)建公共項,使子模塊的內(nèi)部部分暴露給上級模塊。
讓我們回頭看一下示例 7-4 的錯誤,它告訴我們 hosting
模塊是私有的。我們想讓父模塊中的 eat_at_restaurant
函數(shù)可以訪問子模塊中的 add_to_waitlist
函數(shù),因此我們使用 pub
關鍵字來標記 hosting
模塊,如示例 7-5 所示。
文件名: src/lib.rs
mod front_of_house {
pub 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-5: 使用 pub
關鍵字聲明 hosting
模塊使其可在 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` due to 2 previous errors
示例 7-6: 構建示例 7-5 出現(xiàn)的編譯器錯誤
發(fā)生了什么?在 mod hosting
前添加了 pub
關鍵字,使其變成公有的。伴隨著這種變化,如果我們可以訪問 front_of_house
,那我們也可以訪問 hosting
。但是 hosting
的 內(nèi)容(contents) 仍然是私有的;這表明使模塊公有并不使其內(nèi)容也是公有的。模塊上的 pub
關鍵字只允許其父模塊引用它。
示例 7-6 中的錯誤說,add_to_waitlist
函數(shù)是私有的。私有性規(guī)則不但應用于模塊,還應用于結構體、枚舉、函數(shù)和方法。
讓我們繼續(xù)將 pub
關鍵字放置在 add_to_waitlist
函數(shù)的定義之前,使其變成公有。如示例 7-7 所示。
文件名: src/lib.rs
mod front_of_house {
pub mod hosting {
pub 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-7: 為 mod hosting
和 fn add_to_waitlist
添加 pub
關鍵字使他們可以在 eat_at_restaurant
函數(shù)中被調(diào)用
現(xiàn)在代碼可以編譯通過了!讓我們看看絕對路徑和相對路徑,并根據(jù)私有性規(guī)則,再檢查一下為什么增加 pub
關鍵字使得我們可以在 add_to_waitlist
中調(diào)用這些路徑。
在絕對路徑,我們從 crate
,也就是 crate 根開始。然后 crate 根中定義了 front_of_house
模塊。front_of_house
模塊不是公有的,不過因為 eat_at_restaurant
函數(shù)與 front_of_house
定義于同一模塊中(即,eat_at_restaurant
和 front_of_house
是兄弟),我們可以從 eat_at_restaurant
中引用 front_of_house
。接下來是使用 pub
標記的 hosting
模塊。我們可以訪問 hosting
的父模塊,所以可以訪問 hosting
。最后,add_to_waitlist
函數(shù)被標記為 pub
,我們可以訪問其父模塊,所以這個函數(shù)調(diào)用是有效的!
在相對路徑,其邏輯與絕對路徑相同,除了第一步:不同于從 crate 根開始,路徑從 front_of_house
開始。front_of_house
模塊與 eat_at_restaurant
定義于同一模塊,所以從 eat_at_restaurant
中開始定義的該模塊相對路徑是有效的。接下來因為 hosting
和 add_to_waitlist
被標記為 pub
,路徑其余的部分也是有效的,因此函數(shù)調(diào)用也是有效的!
我們還可以使用 super
開頭來構建從父模塊開始的相對路徑。這么做類似于文件系統(tǒng)中以 ..
開頭的語法。我們?yōu)槭裁匆@樣做呢?
考慮一下示例 7-8 中的代碼,它模擬了廚師更正了一個錯誤訂單,并親自將其提供給客戶的情況。fix_incorrect_order
函數(shù)通過指定的 super
起始的 serve_order
路徑,來調(diào)用 serve_order
函數(shù):
文件名: src/lib.rs
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}
fn cook_order() {}
}
示例 7-8: 使用以 super
開頭的相對路徑從父目錄開始調(diào)用函數(shù)
fix_incorrect_order
函數(shù)在 back_of_house
模塊中,所以我們可以使用 super
進入 back_of_house
父模塊,也就是本例中的 crate
根。在這里,我們可以找到 serve_order
。成功!我們認為 back_of_house
模塊和 serve_order
函數(shù)之間可能具有某種關聯(lián)關系,并且,如果我們要重新組織這個 crate 的模塊樹,需要一起移動它們。因此,我們使用 super
,這樣一來,如果這些代碼被移動到了其他模塊,我們只需要更新很少的代碼。
我們還可以使用 pub
來設計公有的結構體和枚舉,不過有一些額外的細節(jié)需要注意。如果我們在一個結構體定義的前面使用了 pub
,這個結構體會變成公有的,但是這個結構體的字段仍然是私有的。我們可以根據(jù)情況決定每個字段是否公有。在示例 7-9 中,我們定義了一個公有結構體 back_of_house:Breakfast
,其中有一個公有字段 toast
和私有字段 seasonal_fruit
。這個例子模擬的情況是,在一家餐館中,顧客可以選擇隨餐附贈的面包類型,但是廚師會根據(jù)季節(jié)和庫存情況來決定隨餐搭配的水果。餐館可用的水果變化是很快的,所以顧客不能選擇水果,甚至無法看到他們將會得到什么水果。
文件名: 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!("I'd like {} toast please", meal.toast);
// 如果取消下一行的注釋代碼不能編譯;
// 不允許查看或修改早餐附帶的季節(jié)水果
// 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
具有私有字段,所以這個結構體需要提供一個公共的關聯(lián)函數(shù)來構造 Breakfast
的實例(這里我們命名為 summer
)。如果 Breakfast
沒有這樣的函數(shù),我們將無法在 eat_at_restaurant
中創(chuàng)建 Breakfast
實例,因為我們不能在 eat_at_restaurant
中設置私有字段 seasonal_fruit
的值。
與之相反,如果我們將枚舉設為公有,則它的所有成員都將變?yōu)楣?。我們只需要?nbsp;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: 設計公有枚舉,使其所有成員公有
因為我們創(chuàng)建了名為 Appetizer
的公有枚舉,所以我們可以在 eat_at_restaurant
中使用 Soup
和 Salad
成員。如果枚舉成員不是公有的,那么枚舉會顯得用處不大;給枚舉的所有成員挨個添加 pub
是很令人惱火的,因此枚舉成員默認就是公有的。結構體通常使用時,不必將它們的字段公有化,因此結構體遵循常規(guī),內(nèi)容全部是私有的,除非使用 pub
關鍵字。
還有一種使用 pub
的場景我們還沒有涉及到,那就是我們最后要講的模塊功能:use
關鍵字。我們將先單獨介紹 use
,然后展示如何結合使用 pub
和 use
。
更多建議: