我們先前所練習的Rust程式幾乎都只是把程式敘述寫在main.rs檔案中,雖然我們已經會使用函數和方法來分割不同功能的程式,但當程式愈寫愈多的時候,這樣的作法還是會讓程式變得難以維護。這時就需要用到Rust提供的「模組」系統了。
模組的使用方式就跟我們撰寫函數的方式差不多,我們可以將類似功能的不同函數、方法和其所使用到的結構體、列舉包裝成相同的模組。模組就是一個型別和函數的「命名空間」(namespace),命名空間可以將程式碼有效區隔開來,不互相影響。模組除了可以將型別和函數群組化之外,還可以決定模組要不要公開給其他專案使用。
模組和程式專案的檔案結構有關,讓我們從建立程式專案開始吧!以往我們建立Cargo程式專案的時候都加了--bin參數,讓Cargo建立出可執行的應用程式專案,但是現在要將其改為--lib參數,讓Cargo建立出函式庫的程式專案。
使用以下指令,可以建立出Cargo的函式庫程式專案communicator:
接著移動工作目錄到專案根目錄下。由於這個是函式庫程式專案,Cargo並沒有在src目錄中產生main.rs,取而代之的是,它產生出了一個陌生的lib.rs,檔案內容如下:
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
這段程式我們先不管它,將它留在lib.rs檔案的最底部。由於現在使用的專案沒有main方法了,因此不可以使用cargo run來執行程式專案。如果要測試程式碼有沒有撰寫正確,先使用cargo check吧!
我們打算讓communicator專案處理網路相關的通訊問題,一開始我們先使用mod關鍵字來定義出一個名叫network的模組,並且在模組內再定義一個connect函數。程式如下:
mod network {
fn connect() {}
}
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
如果要在network模組外呼叫network模組中的connect函數,需要透過::語法來呼叫。程式碼如下:
network::connect();
當然我們也可以使用多個mod關鍵字建立出多個不同的模組。如下:
mod network {
fn connect() {}
}
mod client {
fn connect() {}
}
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
network模組和client模組內都有connect函數,它們的connect函數在使用上不會有衝突。如果要呼叫network模組的connect函數,程式敘述就寫network::connect();如果要呼叫client模組的connect函數,程式敘述就寫client::connect()。
雖然我們現在正在使用Cargo的函式庫專案,但「模組」其實也可以被定義在應用程式專案上,並沒有限制。此外,模組內也可以再有其它的「子模組」。舉例來說:
mod network {
fn connect() {}
mod client {
fn connect() {}
}
}
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
以上程式有兩個不同的connect函數,分別是network::connect和network::client::connect。此時模組的組織架構如下:
communicator
└── network
└── client
當communicator專案作為crate被引用到其它專案時,connect函數的呼叫方式就變成communicator::network::connect()和communicator::network::client::connect()。當然,目前的程式碼實際上無法讓network模組直接被引用到其它專案,還必須與pub關鍵字搭配使用才行,這部份將在之後介紹。
現在雖然我們已經知道怎麼將程式碼用mod關鍵字切成模組了,但如果通通都寫在lib.rs檔案內,累積起來後也是會因太多而難以維護。因此,我們要將模組寫在不同的檔案內!
先看看以下程式:
mod client {
fn connect() {}
}
mod network {
fn connect() {}
mod server {
fn connect() {}
}
}
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
模組的組織架構如下:
communicator
├── client
└── network
└── server
一步一步進行,把client、network和server都分到不同的檔案中。首先,習慣上,為避免lib.rs的內容過多,我們只會在lib.rs中留下要定義的模組名稱,程式如下:
mod client;
mod network;
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Cargo在編譯函式庫專案的時候會先來看lib.rs檔案,當它發現lib.rs檔案有定義模組名稱卻未實作的時候,會有以下兩種情形發生:
模組名稱.rs檔案的話,就會引用進來。2. 如果同目錄中有
模組名稱目錄,且同目錄中沒有模組名稱.rs檔案的話,就會引用模組名稱目錄中的mod.rs檔案。如果第一種和第二種情形同時發生,會編譯錯誤。我們讓client模組用第一種方式引用進來,而network則用第二種方式。引用的情形會像是底下這個樣子:
mod client {
// content from src/client.rs
}
mod network {
// content from src/network/mod.rs
}
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
在lib.rs檔案中定義要使用的模組名稱後,在同樣的src目錄中建立出client.rs檔案,內容如下:
fn connect() {}
這裡要注意的是,用原始碼檔案將模組的程式碼分開之後,該模組的原始碼檔案中就不用再寫mod關鍵字了。換句話說就是,因為client.rs已經定義了這支程式是一個名為client的模組,我們就不再需要寫mod client {}來定義這個模組內的東西。
到這裡我們已經完成client模組的部份,接下來看看network模組和其子模組server要怎麼做吧!
在src目錄中建立network目錄,並在network目錄中建立mod.rs檔案,內容如下:
fn connect() {}
mod server;
注意程式第3行,我們又再度使用mod關鍵字定義模組名稱,而不將它實作出來,所以又要按照剛才提到的那兩個規則引用其它檔案啦!
由於server模組並沒有子模組,因此我們可以直接在目前的network目錄中再建立server.rs檔案,內容如下:
fn connect() {}
如此一來就已經成功將所有模組分散到不同檔案了!
實際上用第一種方式來建立模組也可以讓它擁有子模組,只需要在旁邊建立出與該模組同名的目錄,並在該目錄中放置子模組的原始碼檔案即可,不過要注意的是不要使用到mod.rs這個檔名。不過通常我們還是會直接使用第二種,也就是目錄的方式來設計模組和它的子模組,因為這樣結構會比較清楚。例如剛才的練習的client模組,因為現在它並沒有用到子模組,因此我們可以直接在src目錄下建立client.rs檔案來實作它。
嘗試使用cargo check檢查目前的communicator專案,編譯雖然成功,但是有這個警告訊息function `connect` is never used。
這是因為目前三個模組中的connect函數,都是私有的狀態,且也沒有被呼叫過任何一次。為了解決這個警告,且讓引用這個communicator專案的專案也能使用這三個模組中的connect函數,我們必須使用pub關鍵字,將connect函數的存取權限設為公開的。用pub關鍵字將程式修改如下:
pub fn connect() {}
pub fn connect() {}
mod server;
pub fn connect() {}
重新使用cargo check編譯程式專案,編譯雖然成功,但會發現剛才的警告訊息function `connect` is never used依然存在,這是因為如果我們不將子模組和模組也都設為公開的話,子模組外和communicator專案外根本還是沒有辦法呼叫到三個模組中的connect函數。
用pub關鍵字將程式修改如下:
pub fn connect() {}
pub mod server;
pub mod client;
pub mod network;
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
修改後再次使用cargo check編譯程式專案,編譯成功,警告訊息也沒了!
沒有使用pub關鍵字設定為公開的模組、函數、型別等項目都會被當作是私有的,而私有的項目之就只有在該模組底下或是該模組的子模組才能存取。另外,使用use關鍵字將命名空間加入至目前程式的scope,其命名空間內的公開資源對於目前程式的scope來說是私有的,如非在use關鍵字前也加上pub關鍵字。舉例來說:
mod outermost {
pub fn middle_function() {}
fn middle_secret_function() {}
mod inside {
pub fn inner_function() {}
fn secret_function() {}
}
}
fn try_me() {
outermost::middle_function();
outermost::middle_secret_function();
outermost::inside::inner_function();
outermost::inside::secret_function();
}
以上程式,第15、16、17行程式會編譯錯誤。第15行程式因為middle_secret_function函數不是公開的所以編譯錯誤,而第16、17行程式因為inside子模組不是公開的所以編譯錯誤。
若將inside子模組加上pub關鍵字改為公開的,程式如下:
mod outermost {
pub fn middle_function() {}
fn middle_secret_function() {}
pub mod inside {
pub fn inner_function() {}
fn secret_function() {}
}
}
fn try_me() {
outermost::middle_function();
outermost::middle_secret_function();
outermost::inside::inner_function();
outermost::inside::secret_function();
}
第16行程式會編譯成功,而第17行程式則因為secret_function函數不是公開的,所以編譯錯誤。
將第15行和第17行程式先註解掉,程式如下:
mod outermost {
pub fn middle_function() {}
fn middle_secret_function() {}
pub mod inside {
pub fn inner_function() {}
pub fn secret_function() {}
}
}
fn try_me() {
outermost::middle_function();
// outermost::middle_secret_function();
outermost::inside::inner_function();
// outermost::inside::secret_function();
}
既然剛才提到私有項目可以給子模組使用,那如果我們想在inner_function函數中呼叫middle_secret_function函數,有辦法做到嗎?程式敘述直接寫middle_secret_function()?
mod outermost {
pub fn middle_function() {}
fn middle_secret_function() {}
pub mod inside {
pub fn inner_function() {
middle_secret_function();
}
pub fn secret_function() {}
}
}
fn try_me() {
outermost::middle_function();
// outermost::middle_secret_function();
outermost::inside::inner_function();
// outermost::inside::secret_function();
}
以上程式第8行,我們直接將程式敘述寫成middle_secret_function(),卻造成編譯錯誤,這是因為我們必須要把命名空間寫清楚,指定是要呼叫哪裡的middle_secret_function函數。程式改寫如下:
mod outermost {
pub fn middle_function() {}
fn middle_secret_function() {}
pub mod inside {
pub fn inner_function() {
crate::outermost::middle_secret_function();
}
pub fn secret_function() {}
}
}
fn try_me() {
outermost::middle_function();
// outermost::middle_secret_function();
outermost::inside::inner_function();
// outermost::inside::secret_function();
}
以上程式可以編譯成功。程式第8行,直接以crate::語法開頭來指定命名空間,代表要從目前這個程式的crate開始算起;crate::outermost代表要指定使用目前這個程式的crate底下的outermost模組。
此外,也可以利用super關鍵字來指定回到上一層的命名空間,程式改寫如下:
mod outermost {
pub fn middle_function() {}
fn middle_secret_function() {}
pub mod inside {
pub fn inner_function() {
super::middle_secret_function();
}
pub fn secret_function() {}
}
}
fn try_me() {
outermost::middle_function();
// outermost::middle_secret_function();
outermost::inside::inner_function();
// outermost::inside::secret_function();
}
以上程式可以編譯成功。程式第8行,super::表示要使用inside模組的上一層命名空間,也就是outermost。
除了crate::和super::語法之外,我們也可以直接以::開頭。在Rust 2015的版本中,::的功能和crate::是一樣的,但是在Rust 2018的版本中,::的功能只能夠用來引用外部crate下的項目。
完成我們的函式庫程式專案後,該怎麼將它引用至可執行應用程式專案中呢?繼續拿剛才完成的communicator函式庫程式專案來舉例,我們先用Cargo產生一個可執行的應用程式專案communicator_exe,指令如下:
編輯communicator_exe程式專案的Cargo.toml設定檔,在dependencies區塊加入communicator程式專案的設定,如下:
[package]
name = "communicator_exe"
version = "0.1.0"
edition = "2021"
[dependencies]
communicator = { path = "../communicator" }
此時,若要在communicator_exe專案中呼叫communicator專案下的network::server::connect()函數,程式如下:
fn main() {
communicator::network::server::connect();
}
這邊要注意的是,如果函式庫程式專案的名稱,也就是在函式庫程式專案裡的Cargo.toml設定檔寫的專案名稱為communicator的話,在被其它專案引用時,Cargo.toml也要使用相同的名稱,不能隨意更改。但如果要更改的話,可以在Cargo.toml的dependencies區塊中使用package選項來指定要相依的套件名稱(該函式庫原本的名稱),如此一來就我們可以自訂要用什麼名稱來表示該函式庫了。如下設定,在communicator_exe專案中,可以使用comm這個名稱來表示communicator函式庫專案。
[package]
name = "communicator_exe"
version = "0.1.0"
edition = "2021"
[dependencies]
com = { package = "communicator", path = "../communicator" }
如此一來,若要在communicator_exe專案中呼叫communicator::network::server::connect()函數,就變成要寫成com::network::server::connect()。如下:
fn main() {
com::network::server::connect();
}
我們也可以利用use關鍵字和as關鍵字在特定scope下改變crate、函數、型別等項目的名稱。如下:
例如:
use communicator as com;
fn main() {
com::network::server::connect();
}
即便我們利用上面的方式將程式碼稍微縮短了,但com::network::server::connect()這樣的語法還是很長,不太好寫。這時可以考慮利用use關鍵字,將指定命名空間下的函數、型別等項目加入至目前程式的scope。舉例來說:
use communicator as com;
use com::client;
use com::network;
use network::server;
fn main() {
server::connect();
}
use關鍵字可以搭配{}語法使用,可以一次將某個命名空間下的多個項目加進程式。舉例來說:
use communicator as com;
use com::{client, network};
use network::server;
fn main() {
server::connect();
}
也支援巢狀用法。舉例來說:
use communicator as com;
use com::{client, network, network::server};
fn main() {
server::connect();
}
use關鍵字甚至能直接使用星號*,來表示某個命名空間下的所有項目。舉例來說:
use communicator as com;
use com::*;
use network::*;
fn main() {
server::connect();
}
最後我們來看一下communicator程式專案的lib.rs,在一開始我們忽略的由Cargo自動產生出來的tests模組。程式如下:
pub mod client;
pub mod network;
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
我們之前都忽略了lib.rs中的tests,將其考慮進來後,communicator程式專案的組織架構如下:
communicator ├── client ├── network | └── client └── tests
因為有在tests模組定義前使用#[cfg(test)],因此這個tests模組只會在我們使用cargo test來測試程式專案的時候才會被用到。當然tests模組的名稱其實不一定要叫作tests。這個加上#[cfg(test)]定義出來模組,就像是一個擁有許多測試案例的函數集合,定義在這個模組中的函數,如果再加上#[test]的話,在使用cargo test來測試程式專案的時候,該函數就會被執行。
所以現在我們的communicator程式專案,使用cargo test進行測試的結果如下圖:
我們可以在tests模組中,搭配use關鍵字來將我們要測試的其它模組加入至tests模組的scope,方便進行測試。修改後的程式如下:
pub mod client;
pub mod network;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
client::connect();
network::connect();
network::server::connect();
}
}
有關於更詳細的程式專案測試方式,會在之後的章節作介紹。
總結
學習完這個章節的內容後,我們已經有能力組織管理我們的Rust程式碼,甚至還可以將我們撰寫的函式庫程式專案分享給其他人使用了。在接下來的章節,我們要來學習Rust程式語言內建的Vec、HashMap等常用的集合資料結構。
下一章:常用的集合。







