我們先前所練習的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
等常用的集合資料結構。
下一章:常用的集合。