模塊系統(tǒng)
在編寫較為復(fù)雜的項(xiàng)目時(shí),合理地對代碼進(jìn)行組織與管理非常重要。只有按照不同的特性來組織或分割相關(guān)功能的代碼,才能夠清晰地找到實(shí)現(xiàn)指定功能的代碼片段,確定哪些地方需要修改。
除了對功能進(jìn)行分組,對實(shí)現(xiàn)的細(xì)節(jié)進(jìn)行封裝可以使開發(fā)者在更高的層次上復(fù)用代碼:一旦實(shí)現(xiàn)了某個(gè)功能,其他代碼就可以通過公共接口調(diào)用這個(gè)操作,而無需了解具體的實(shí)現(xiàn)細(xì)節(jié)。
Rust 提供了一系列的功能來管理代碼,包括決定哪些細(xì)節(jié)是暴露的,那些細(xì)節(jié)是私有的,以及不同的作用域內(nèi)存在哪些名稱。這些功能被統(tǒng)稱為模塊系統(tǒng):
- 包(package):一個(gè)用于構(gòu)建、測試并分享單元包的 Cargo 特性
- 單元包(crate):一個(gè)用于生成庫或可執(zhí)行文件的樹形模塊結(jié)構(gòu)
- 模塊(module)及 use 關(guān)鍵字:用于控制文件結(jié)構(gòu)、作用域及路徑的私有性
- 路徑(path):一種用于命名條目的方法,這些條目包括結(jié)構(gòu)體、函數(shù)和模塊等
包與單元包
當(dāng)我們使用 cargo new 命令創(chuàng)建新項(xiàng)目時(shí),如:
cargo new restaurant
Cargo 會自動創(chuàng)建如下結(jié)構(gòu)的 Rust 項(xiàng)目:
restaurant
├── Cargo.toml
└── src
└── main.rs
Cargo 默認(rèn)會將自動生成的 src/main.rs 源文件視作一個(gè)二進(jìn)制單元包(crate)的根節(jié)點(diǎn),與包(package)擁有相同的名稱(即 restaurant)。
假設(shè)包的目錄中包含文件 src/lib.rs,Cargo 也會自動將其視作與包同名的庫單元包的根節(jié)點(diǎn)。
可以在路徑 src/bin 下添加源文件來創(chuàng)建更多的二進(jìn)制單元包,這些源文件都會被視作獨(dú)立的二進(jìn)制單元包。
自動生成的 src/main.rs 源文件內(nèi)容如下:
fn main() {
println!("Hello, world!");
}
我們可以創(chuàng)建一個(gè) src/lib.rs 源文件,把上面的打印輸出的操作作為公共函數(shù)定義在 lib.rs 中,再在 main.rs 中調(diào)用該公共函數(shù),效果與之前是一致的。
src/lib.rs 代碼:
pub fn greeting() {
println!("Hello, World!");
}
src/main.rs 代碼:
fn main() {
restaurant::greeting();
}
因?yàn)?lib.rs 默認(rèn)會作為一個(gè)與包同名(都叫 restaurant)的庫單元包(crate)存在,且其中的 greeting 函數(shù)已被聲明為公開的(pub),因此可以直接在 main.rs 中使用 restaurant::greeting() 調(diào)用 lib.rs 中定義的 greeting 函數(shù)。
使用 cargo run 命令運(yùn)行項(xiàng)目后,target/debug 路徑下除了像之前一樣生成 restaurant 可執(zhí)行文件外,還會額外生成 librestaurant.rlib 庫文件。
單元包可以將相關(guān)的功能分組,并放到同一作用域下,這樣便可以使這些功能輕松地在多個(gè)項(xiàng)目中共享。
將單元包的功能保留在它們自己的作用域中有助于指明某個(gè)特定功能來源于哪個(gè)單元包,并避免可能的命名沖突。
比如 rand 包提供了一個(gè)名為 Rng 的 trait,我們同樣也可以在自己的單元包中定義一個(gè)名為 Rng 的結(jié)構(gòu)體。正是由于這些功能被放置在了各自的作用域中,我們能夠使用 rng::Rng 訪問 rand 包中提供的 Rng trait,而 Rng 則指向剛剛創(chuàng)建的 Rng 結(jié)構(gòu)體。
通過定義模塊來控制作用域及私有性
假設(shè)我們需要編寫一個(gè)提供就餐服務(wù)的庫單元包。一個(gè)現(xiàn)實(shí)的店面常常會劃分為前廳與后廚兩個(gè)部分,前廳負(fù)責(zé)點(diǎn)單和結(jié)賬等,后廚則負(fù)責(zé)制作料理。
為了按照餐廳的實(shí)際工作方式來組織單元包,可以將函數(shù)放置在嵌套的模塊中。修改 src/lib.rs 源代碼文件,內(nèi)容如下:
mod front_of_house {
mod hosting {
fn seat_at_table() {
println!("Seat at table.");
}
}
mod serving {
fn take_order() {
println!("Taking order.");
}
fn take_payment() {
println!("Taking payment.");
}
}
}
我們可以使用 mod 關(guān)鍵字來定義一個(gè)模塊(如本例中的 front_of_house),模塊內(nèi)還可以繼續(xù)定義其他模塊(如本例中的 hosting 和 serving)。模塊內(nèi)同樣也可以包含其他條目的定義,如結(jié)構(gòu)體、枚舉、常量、trait 或函數(shù)等。
src/main.rs 與 src/lib.rs 被稱作單元包(crate)的根節(jié)點(diǎn),它們的內(nèi)容各自組成了一個(gè)名為 crate 的模塊。這個(gè)模塊的結(jié)構(gòu)也被稱為模塊樹。
上面 src/lib.rs 形成的樹狀模塊結(jié)構(gòu)如下:
crate
└── front_of_house
├── hosting
│ └── seat_at_table
└── serving
├── take_order
└── take_payment
路徑
類似于在文件系統(tǒng)中使用路徑進(jìn)行導(dǎo)航,在 Rust 的模塊樹中定位某個(gè)條目同樣需要使用路徑。
路徑有兩種形式:
- 使用單元包名或字面量 crate 從根節(jié)點(diǎn)開始的絕對路徑
- 使用
self、super或內(nèi)部標(biāo)識符從當(dāng)前模塊開始的相對路徑
絕對路徑與相對路徑都至少由一個(gè)標(biāo)識符組成,標(biāo)識符之間使用雙冒號(::)分隔。
現(xiàn)在嘗試在模塊外部調(diào)用模塊中定義的函數(shù)。在 src/lib.rs 末尾添加一個(gè)公共函數(shù) eat_at_restaurant,調(diào)用模塊 front_of_house 中定義的函數(shù):
pub fn eat_at_restaurant() {
// 絕對路徑
crate::front_of_house::hosting::seat_at_table();
// 相對路徑
front_of_house::serving::take_order();
}
修改 src/main.rs,在 main 函數(shù)中調(diào)用上一步中定義的公共函數(shù):
fn main() {
restaurant::eat_at_restaurant();
}
嘗試編譯項(xiàng)目,會報(bào)出如下錯(cuò)誤:
error[E0603]: module `hosting` is private
error[E0603]: module `serving` is private
即模塊 hosting 和 serving 是私有的,Rust 不允許我們訪問。
Rust 中的模塊不僅僅用于組織代碼,同時(shí)也定義了私有邊界:外部代碼無法知曉、調(diào)用或依賴那些由私有邊界封裝了的實(shí)現(xiàn)細(xì)節(jié)。
Rust 中的所有條目(函數(shù)、方法、結(jié)構(gòu)體、枚舉、模塊及常量)默認(rèn)都是私有的,處于父級模塊中的條目無法使用子模塊中的私有條目,但子模塊中的條目可以使用其祖先模塊中的條目。
Rust 希望默認(rèn)隱藏內(nèi)部的實(shí)現(xiàn)細(xì)節(jié),這樣用戶就能明確地知道修改哪些內(nèi)容不會破壞外部代碼。
使用 pub 關(guān)鍵字暴露路徑
可以使用 pub 關(guān)鍵字將某些條目標(biāo)記為公共的,從而使子模塊中的這些部分可以被暴露到祖先模塊中。
接上面的例子,為了使父模塊中的 eat_at_restaurant 函數(shù)能夠正常訪問子模塊中定義的函數(shù),可以使用 pub 關(guān)鍵字來標(biāo)記 hosting 和 serving 模塊。
需要注意的是,模塊被 pub 標(biāo)記,其效果僅限于模塊本身,并不會影響到它內(nèi)部條目的狀態(tài),模塊中的內(nèi)容依舊是私有的。為了使前面的代碼正常工作,還必須在需要公開的函數(shù)前面添加 pub 關(guān)鍵字。
編輯 src/lib.rs 中,內(nèi)容改動如下:
mod front_of_house {
pub mod hosting {
pub fn seat_at_table() {
println!("Seat at table.");
}
}
pub mod serving {
pub fn take_order() {
println!("Taking order.");
}
pub fn take_payment() {
println!("Taking payment.");
}
}
}
pub fn eat_at_restaurant() {
// 絕對路徑
crate::front_of_house::hosting::seat_at_table();
// 相對路徑
front_of_house::serving::take_order();
}
此時(shí)程序即可以正常運(yùn)行。
將結(jié)構(gòu)體聲明為公共的
當(dāng)我們在結(jié)構(gòu)體定義前使用 pub 關(guān)鍵字時(shí),結(jié)構(gòu)體本身就成為了公共結(jié)構(gòu)體,但是它的字段依舊保持私有狀態(tài)。
我們可以逐一決定是否將某個(gè)字段公開。
下面的代碼定義了一個(gè)公共的 back_of_house::Breakfast 結(jié)構(gòu)體,并令其 toast 字段公開,而 seasonal_fruit 字段保持私有。使得客戶可以自行選擇想要的面包,而只有廚師才能根據(jù)季節(jié)與存貨決定配餐水果。
編輯 src/lib.rs 源文件,添加如下 back_of_house 模塊:
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"),
}
}
}
}
為了測試新添加的代碼能否正常工作,修改 src/lib.rs 中的 eat_at_restaurant 函數(shù)如下:
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");
}
back_of_house::Breakfast 結(jié)構(gòu)體中的 toast 字段是公共的,我們因此能夠在 eat_at_restaurant 中使用點(diǎn)號讀寫 toast 字段。
同樣由于 seasonal_fruit 字段是私有的,我們不能在 eat_at_restaurant 中訪問它。
另外,由于 back_of_house::Breakfast 擁有一個(gè)私有字段,這個(gè)結(jié)構(gòu)體必須提供一個(gè)公共的關(guān)聯(lián)函數(shù)來構(gòu)造 Breakfast 實(shí)例(本例中的 summer),否則我們將無法在結(jié)構(gòu)體外部創(chuàng)建任何的 Breakfast 實(shí)例。
用 use 關(guān)鍵字將路徑導(dǎo)入作用域
基于路徑來調(diào)用函數(shù)的寫法看上去會有些重復(fù)與冗長。無論我們使用絕對路徑還是相對路徑來指定 seat_at_table 函數(shù),都必須在每次調(diào)用時(shí)指定路徑上的 front_of_house 和 hosting 節(jié)點(diǎn)。
可以借助 use 關(guān)鍵字將路徑引入作用域,簡化上述步驟。如:
// src/lib.rs
// ...
// 絕對路徑
use crate::front_of_house::hosting;
// 相對路徑
use self::front_of_house::serving;
pub fn eat_at_restaurant() {
hosting::seat_at_table();
serving::take_order();
}
在作用域中使用 use 引入路徑有點(diǎn)類似于在文件系統(tǒng)中創(chuàng)建符號鏈接。通過在單元包的根節(jié)點(diǎn)下添加上述兩條 use 語句,hosting 和 serving 成了該作用域下的一個(gè)有效名稱,就如同這兩個(gè)模塊被定義在根節(jié)點(diǎn)下一樣。
這里使用了 use crate::front_of_house::hosting 并接著調(diào)用 hosting::seat_at_table,而沒有使用 use crate::front_of_house::hosting::seat_at_table 來直接引入 seat_at_table 函數(shù)。
相對而言,前者的方式更常用一些。使用 use 將函數(shù)的父模塊引入作用域,意味著我們必須在調(diào)用函數(shù)時(shí)指定這個(gè)父模塊,從而更清晰地表明當(dāng)前函數(shù)沒有被定義在當(dāng)前作用域中。
不同于函數(shù),使用 use 將結(jié)構(gòu)體、枚舉或其他條目引入作用域時(shí),我們習(xí)慣于通過指定完整路徑的方式引入。
使用 as 提供新的名稱
使用 use 將多個(gè)同名類型引入作用域時(shí),還可以在路徑后使用 as 關(guān)鍵字為類型指定一個(gè)新的本地名稱,也就是別名。如:
use std::fmt::Result;
use std::io::Result as IoResult;
使用 pub use 重導(dǎo)出名稱
當(dāng)我們使用 use 關(guān)鍵字將名稱引入作用域時(shí),這個(gè)名稱會以私有的方式在新的作用域中生效。為了讓外部代碼能夠訪問到這些名稱,可以通過組合使用 pub 和 use 修飾其路徑。
這項(xiàng)技術(shù)也被稱作重導(dǎo)出。
比如使用 pub 修飾前面 src/lib.rs 中的某條 use 語句:
pub use crate::front_of_house::hosting;
于是在另一個(gè)文件 src/main.rs 中也就可以使用 restaurant::hosting::seat_at_table() 形式的代碼調(diào)用 hosting 模塊中的函數(shù)了。
通過使用 pub use,我們可以在編寫代碼時(shí)使用一種結(jié)構(gòu),在對外暴露時(shí)使用另外一種不同的結(jié)構(gòu)。這一方法可以讓我們的代碼庫對編寫者和調(diào)用者同時(shí)保持良好的組織結(jié)構(gòu)。
將模塊拆分為不同的文件
當(dāng)模塊規(guī)模逐漸增大時(shí),我們可以將它們的定義移動到新的文件中。
比如我們需要將 src/lib.rs 中定義的 front_of_house 模塊移動到它自己的文件 src/front_of_house.rs 中。首先將根節(jié)點(diǎn)文件 lib.rs 中的代碼改為如下版本:
mod front_of_house;
pub use self::front_of_house::serving;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::seat_at_table();
serving::take_order();
}
在 mod front_of_house 后使用分號而不是代碼塊,會讓 Rust 前往與當(dāng)前模塊同名的文件中加載模塊內(nèi)容。因此可以將 front_of_house 模塊的具體定義轉(zhuǎn)移到 src/front_of_house.rs 文件中,效果是一樣的。
// src/front_of_house.rs
pub mod hosting {
pub fn seat_at_table() {
println!("Seat at table.");
}
}
pub mod serving {
pub fn take_order() {
println!("Taking order.");
}
pub fn take_payment() {
println!("Taking payment.");
}
}
事實(shí)上還可以更進(jìn)一步,繼續(xù)拆解 front_of_house 模塊到其他文件中。首先將 src/front_of_house.rs 文件的內(nèi)容改為如下版本:
pub mod hosting;
pub mod serving;
接著創(chuàng)建一個(gè) src/front_of_house 目錄,以及一個(gè) src/front_of_house/hosting.rs 文件用來存放 hosting 模塊的定義,一個(gè) src/front_of_house/serving.rs 文件存放 serving 模塊的定義:
// src/front_of_house/hosting.rs
pub fn seat_at_table() {
println!("Seat at table.");
}
// src/front_of_house/serving.rs
pub fn take_order() {
println!("Taking order.");
}
pub fn take_payment() {
println!("Taking payment.");
}
最終效果與前兩種版本也是一致的。
此時(shí) restaurant 項(xiàng)目的目錄結(jié)構(gòu)如下:
restaurant
├── Cargo.lock
├── Cargo.toml
└── src
├── front_of_house
│ ├── hosting.rs
│ └── serving.rs
├── front_of_house.rs
├── lib.rs
└── main.rs
所有的修改都沒有改變原有的模塊樹結(jié)構(gòu),盡管這些定義被放置到了不同的文件中,eat_at_restaurant 中的函數(shù)調(diào)用依舊有效。