我們先前所練習的Rust程式幾乎都只是把程式敘述寫在「main.rs」檔案中,雖然我們已經會使用函數和方法來分割不同功能的程式,但當程式愈寫愈多的時候,這樣的作法還是會讓程式變得難以維護。這時就需要用到Rust提供的「模組」系統了。

模組的使用方式就跟我們撰寫函數的方式差不多,我們可以將類似功能的不同函數、方法和其所使用到的結構體、列舉包裝成相同的模組。模組就是一個型別和函數的「命名空間」(namespace),命名空間可以將程式碼有效區隔開來,不互相影響。模組除了可以將型別和函數群組化之外,還可以決定模組要不要公開給其他專案使用。

模組和程式專案的檔案結構有關,讓我們從建立程式專案開始吧!以往我們建立Cargo程式專案的時候都加了「--bin」參數,讓Cargo建立出可執行的應用程式專案,但是現在要將其改為「--lib」參數,讓Cargo建立出函式庫的程式專案。

使用以下指令,可以建立出Cargo的函式庫程式專案「communicator」:

cargo new --lib communicator

rust-module

接著移動工作目錄到專案根目錄下。由於這個是函式庫程式專案,Cargo並沒有在「src」目錄中產生「main.rs」,取而代之的是,它產生出了一個陌生的「lib.rs」,檔案內容如下:

這段程式我們先不管它,將它留在「lib.rs」檔案的最底部。由於現在使用的專案沒有main方法了,因此不可以使用「cargo run」來執行程式專案。如果要測試程式碼有沒有撰寫正確,先使用「cargo check」吧!

我們打算讓「communicator」專案處理網路相關的通訊問題,一開始我們先使用「mod」關鍵字來定義出一個名叫「network」的模組,並且在模組內再定義一個「connect」函數。程式如下:

如果要在「network」模組外呼叫「network」模組中的「connect」函數,需要透過「::」語法來呼叫。程式碼如下:

當然我們也可以使用多個「mod」關鍵字建立出多個不同的模組。如下:

「network」模組和「client」模組內都有「connect」函數,它們的「connect」函數在使用上不會有衝突。如果要呼叫「network」模組的「connect」函數,程式敘述就寫「network::connect()」;如果要呼叫「client」模組的「connect」函數,程式敘述就寫「client::connect()」。

雖然我們現在正在使用Cargo的函式庫專案,但「模組」其實也可以被定義在應用程式專案上,並沒有限制。此外,模組內也可以再有其它的「子模組」。舉例來說:

以上程式有兩個不同的「connect」函數,分別是「network::connect」和「network::client::connect」。此時模組的組織架構如下:

當「communicator」專案作為crate被引用到其它專案時,「connect」函數的呼叫方式就變成「communicator::network::connect()」和「communicator::network::client::connect()」。當然,目前的程式碼實際上無法讓「network」模組直接被引用到其它專案,還必須與「pub」關鍵字搭配使用才行,這部份將在之後介紹。

現在雖然我們已經知道怎麼將程式碼用「mod」關鍵字切成模組了,但如果通通都寫在「lib.rs」檔案內,累積起來後也是會因太多而難以維護。因此,我們要將模組寫在不同的檔案內!

先看看以下程式:

模組的組織架構如下:

一步一步進行,把「client」、「network」和「server」都分到不同的檔案中。首先,習慣上,為避免「lib.rs」的內容過多,我們只會在「lib.rs」中留下要定義的模組名稱,程式如下:

Cargo在編譯函式庫專案的時候會先來看「lib.rs」檔案,當它發現「lib.rs」檔案有定義模組名稱卻未實作的時候,會有以下兩種情形發生:

1. 如果同目錄中有「模組名稱.rs」檔案的話,就會引用進來。
2. 如果同目錄中有「模組名稱」目錄的話,就會引用其中的「mod.rs」檔案。

第一種引用的方式只適用於該模組沒有子模組的情況,第二種引用的方式則不管模組有沒有子模組都適用。我們讓「client」模組用第一種方式引用進來,而「network」則用第二種方式。引用的情形會像是底下這個樣子:

在「lib.rs」檔案中定義要使用的模組名稱後,在同樣的src目錄中建立出「client.rs」檔案,內容如下:

這裡要注意的是,用原始碼檔案將模組的程式碼分開之後,該模組的原始碼檔案中就不用再寫「mod」關鍵字了。

到這裡我們已經完成「client」模組的部份,接下來看看「network」模組和其子模組「server」要怎麼做吧!

在src目錄中建立「network」目錄,並在「network」目錄中建立「mod.rs」檔案,內容如下:

注意程式第4行,我們又再度使用「mod」關鍵字定義模組名稱,而不將它實作出來,所以又要按照剛才提到的那兩個規則引用其它檔案啦!

由於「server」模組並沒有子模組,因此我們可以直接在目前的「network」目錄中再建立「server.rs」檔案,內容如下:

如此一來就已經成功將所有模組分散到不同檔案了!

其實為了避免現在沒有用到子模組的模組在之後也用了子模組,導致我們還要改動程式專案的檔案結構,通常會直接考慮使用第二種,也就是目錄的方式來引用模組。例如剛才的練習的「client」模組,因為現在它並沒有用到子模組,因此我們可以直接在「src」目錄下建立「client.rs」檔案來實作它。但如果未來它需要用到子模組了,我們就必須在「src」目錄下建立「client」目錄,把「client.rs」檔案放入並改名為「mod.rs」,這有多麻煩啊!

至於為什麼有子模組的模組一定要另外放在某個目錄中,而不能直接建立一個跟它同名的檔案來實作它呢?這是因為如果不同模組有相同名稱的子模組的話,程式碼就不知道要怎麼擺了!一個模組一個目錄的話,可以確保它有自己的「命名空間」,才不會被其它模組影響。

嘗試使用「cargo check」檢查目前的「communicator」專案,編譯雖然成功,但是有這個警告訊息「function is never used: connect」。

rust-module

這是因為目前三個模組中的「connect」函數,都是私有的狀態,且也沒有被呼叫過任何一次。為了解決這個警告,且讓引用這個「communicator」專案的專案也能使用這三個模組中的「connect」函數,我們必須使用「pub」關鍵字,將「connect」函數的存取權限設為公開的。用「pub」關鍵字將程式修改如下:

重新使用「cargo check」編譯程式專案,編譯雖然成功,但會發現剛才的警告訊息「function is never used: connect」依然存在,這是因為如果我們不將子模組和模組也都設為公開的話,子模組外和「communicator」專案外根本還是沒有辦法呼叫到三個模組中的「connect」函數。

rust-module

用「pub」關鍵字將程式修改如下:

修改後再次使用「cargo check」編譯程式專案,編譯成功,警告訊息也沒了!

rust-module

沒有使用「pub」關鍵字設定為公開的模組、函數、型別等項目都會被當作是私有的,而私有的項目之就只有在該模組底下或是該模組的子模組才能存取。另外,使用「use」關鍵字將命名空間加入至目前程式的scope,其命名空間內的公開資源對於目前程式的scope來說是私有的,如非在「use」關鍵字前也加上「pub」關鍵字。舉例來說:

以上程式,第15、16、17行程式會編譯錯誤。第15行程式因為「middle_secret_function」函數不是公開的所以編譯錯誤,而第16、17行程式因為「inside」子模組不是公開的所以編譯錯誤。

若將「inside」子模組加上「pub」關鍵字改為公開的,程式如下:

第16行程式會編譯成功,而第17行程式則因為「secret_function」函數不是公開的,所以編譯錯誤。

將第15行和第17行程式先註解掉,程式如下:

既然剛才提到私有項目可以給子模組使用,那如果我們想在「inner_function」函數中呼叫「middle_secret_function」函數,有辦法做到嗎?程式敘述直接寫「middle_secret_function()」?

以上程式第8行,我們直接將程式敘述寫成「middle_secret_function()」,卻造成編譯錯誤,這是因為我們必須要把命名空間寫清楚,指定是要呼叫哪裡的「middle_secret_function」函數。程式改寫如下:

以上程式可以編譯成功。程式第8行,直接以「::」語法開頭來指定命名空間,代表要從這個程式專案開始算起;「::outermost」代表要指定使用這個程式專案底下的「outermost」模組。

此外,也可以利用「super」關鍵字來指定回到上一層的命名空間,程式改寫如下:

以上程式可以編譯成功。程式第8行,「super::」表示要使用「inside」模組的上一層命名空間,也就是「outermost」。

完成我們的函式庫程式專案後,該怎麼將它引用至可執行應用程式專案中呢?繼續拿剛才完成的「communicator」函式庫程式專案來舉例,我們先用Cargo產生一個可執行的應用程式專案「communicator_exe」,指令如下:

cargo new --bin communicator_exe

rust-module

編輯「communicator_exe」程式專案的「Cargo.toml」設定檔,在「dependencies」區塊加入「communicator」程式專案的設定,如下:

這邊要注意的是,如果函式庫程式專案的名稱,也就是在函式庫程式專案裡的「Cargo.toml」設定檔寫的專案名稱為「communicator」的話,在被其它專案引用時,「Cargo.toml」也要使用相同的名稱,不能隨意更改。

然後在要使用「communicator」的原始碼檔案中引用crate的地方加上「extern crate」關鍵字來引用「communicator」程式專案。如下:

例如:

剛才雖然提到引用其它專案時要使用其它專案原本的專案名稱,不能隨意更改。但是,如果該函式庫程式專案裡的「Cargo.toml」設定檔有另外定義「lib」區塊並且有設定「name」的話,當它被引用時,「extern crate」關鍵字就必須依據其「Cargo.toml」設定檔內「lib」區塊設定的「name」來引用。例如我們將「communicator」的「Cargo.toml」修改為:

就要將「communicator_exe」的「main.rs」改為:

即便我們利用「lib」區塊設定的「name」來將程式碼稍微縮短了,但「com::network::server::connect()」這樣的語法還是很長,不太好寫。這時可以考慮利用「use」關鍵字,將指定命名空間下的函數、型別等項目加入至目前程式的scope。舉例來說:

「use」關鍵字可以搭配「{}」語法使用,可以一次將某個命名空間下的多個項目加進程式。舉例來說:

也支援巢狀用法。舉例來說:

「use」關鍵字甚至能直接使用星號「*」,來表示某個命名空間下的所有項目。舉例來說:

最後我們來看一下「communicator」程式專案的「lib.rs」,在一開始我們忽略的由Cargo自動產生出來的「tests」模組。程式如下:

我們之前都忽略了「lib.rs」中的「tests」,將其考慮進來後,「communicator」程式專案的組織架構如下:

因為有在「tests」模組定義前使用「#[cfg(test)]」,因此這個「tests」模組只會在我們使用「cargo test」來測試程式專案的時候才會被用到。當然「tests」模組的名稱其實不一定要叫作「tests」。這個加上「#[cfg(test)]」定義出來模組,就像是一個擁有許多測試案例的函數集合,定義在這個模組中的函數,如果再加上「#[test]」的話,在使用「cargo test」來測試程式專案的時候,該函數就會被執行。

所以現在我們的「communicator」程式專案,使用「cargo test」進行測試的結果如下圖:

rust-module

我們可以在「tests」模組中,搭配「use」關鍵字來將我們要測試的其它模組加入至「tests」模組的scope,方便進行測試。修改後的程式如下:

有關於更詳細的程式專案測試方式,會在之後的章節作介紹。

總結

學習完這個章節的內容後,我們已經有能力組織管理我們的Rust程式碼,甚至還可以將我們撰寫的函式庫程式專案分享給其他人使用了。在接下來的章節,我們要來學習Rust程式語言內建的「Vector」、「HashMap」等常用的集合資料結構。