模塊系統(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)用依舊有效饼灿。