模塊樹中引用項的路徑
為了告訴 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
mod front_of_house {mod hosting {fn add_to_waitlist() {}}
}pub fn eat_at_restaurant() {// Absolute pathcrate::front_of_house::hosting::add_to_waitlist();// Relative pathfront_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 buildCompiling 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
mod front_of_house {pub mod hosting {fn add_to_waitlist() {}}
}// -- snip --
清單7-5:將宿主模塊聲明為pub以便從eat_at_restaurant中使用
不幸的是,清單7-5中的代碼仍然會導致編譯錯誤,如清單7-6所示。
$ cargo buildCompiling 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
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() {// 夏天點一份帶黑麥吐司(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
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 。