Rust入坑指南:有條不紊
- 2019 年 11 月 5 日
- 筆記
隨著我們的坑越來越多,越來越大,我們必須要對各種坑進行管理了。Rust為我們提供了一套坑務管理系統,方便大家有條不紊的尋找、管理、填埋自己的各種坑。
Rust提供給我們一些管理程式碼的特性:
- Packages:Cargo的一個特性,幫助你進行構建、測試和共享crates
- Crates:生成庫或可執行文件的模組樹
- Modules和use:用於控制程式碼組織、範圍和隱私路徑
- Paths:struct、function和module的命名方法
下面我們來具體看一下這些特性是如何幫助我們組織程式碼的。
Packages和Crates
package可以理解為一個項目,而crate可以理解為一個程式碼庫。crate可以供多個項目使用。那我們的項目中package和crate是怎麼定義的呢?
之前我們總是通過IDEA來新建項目,今天我們換個方法,在命令行中使用cargo命令來創建。
$ cargo new hello-world Created binary (application) `hello-world` package $ ls hello-world Cargo.toml src $ ls hello-world/src main.rs
可以看到,我們使用cargo創建項目後,只有兩個文件,Cargo.toml和src目錄下的main.rs。
Cargo.toml是管理項目依賴的文件,每個Cargo.toml定義一個package。main.rs文件的存在表示package中包含一個二進位crate,它是二進位crate的入口文件,crate的名稱和package相同。如果src目錄下存在lib.rs文件,說明package中包含一個和package名稱相同的庫crate。
一個package可以包含多個二進位crate,它們由src/lib目錄下的文件定義。如果你的項目想引用他人的crate,可以在Cargo.toml文件中增加依賴。每個crate都有自己的命名空間,因此如果你引入了一個crate裡面定義了一個名為hello的函數,你仍然可以在自己的crate中再定義一個名為hello的函數。
Module
Module幫助我們在crate中組織程式碼,同時Module也是封裝程式碼的重要工具。接下來還是通過一個栗子來詳細了解Module。
前面我們說過,庫crate定義在src/lib.rs文件中。這裡首先創建一個包含了庫crate的package:
cargo new --lib restaurant
然後在src中定義一些module和函數。
mod front_of_house { mod hosting { fn add_to_waitlist() {} fn seat_at_table() {} } mod serving { fn take_order() {} fn serve_order() {} fn take_payment() {} } }
可以看到我們使用關鍵字mod
來定義Module,Module中可以繼續定義Module或函數。這樣我們就可以比較方便的把相關的函數放到一個Module中,並為Module命名,提高程式碼的可讀性。另外Module中還可以定義struct和枚舉。由於Module中可以嵌套定義子Module,最終我們定義出來的程式碼類似一個樹形。
那麼如何訪問Module中的函數呢?這就要提到Path了。這部分比較好理解,Module樹相當於系統文件目錄,而Path則是目錄的路徑。
Path
這裡的路徑和系統文件路徑一樣,都分為相對路徑和絕對路徑兩種。其中絕對路徑必須以crate
開頭,因為它程式碼整個Module樹的根節點。路徑之間使用的是雙冒號來表示引用。
現在我來嘗試在一個函數中調用add_to_waitlist函數:
可以看到這裡不管用絕對路徑還是相對路徑都報錯了,錯誤資訊是模組hosting和函數add_to_waitlist是私有(private)的。我們先暫時放下這個錯誤,根據這裡的錯誤提示,我們知道了當我們定義一個module時,默認情況下是私有的,我們可以通過這種方法來封裝一些程式碼的實現細節。
OK,回到剛才的問題,那我們怎麼才能解決這個錯誤呢?地球人都知道應該把對應的模組與函數公開出來。Rust中標識模組或函數為公有的關鍵字是pub
。
我們用pub關鍵字來把對應的模組和函數公開
這樣我們就可以在module外來調用module內的函數了。
Rust中的私有規則
現在我們再回過頭來看Rust中的一些私有規則,如果你試驗了上面的例子,也許會有一些發現。
Rust中私有規則適用於所有項(函數、方法、結構體、枚舉、模組和常量),它們默認都是私有的。父模組中的項不能訪問子模組中的私有項,而子模組中的項可以訪問其祖輩(父模組及以上)中的項。
Struct和Enum的私有性
Struct和Enum的私有性略有不同,對於Struct來講,我可以只將其中的某些欄位設置為公有的,其他欄位可以仍然保持私有。
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"), } } } } pub fn eat_at_restaurant() { // Order a breakfast in the summer with Rye toast let mut meal = back_of_house::Breakfast::summer("Rye"); // Change our mind about what bread we'd like meal.toast = String::from("Wheat"); println!("I'd like {} toast please", meal.toast); }
而對於Enum,如果一個Enum是公有的,那麼它的所有值都是公有的,因為私有的值沒有意義。
相對路徑和絕對路徑的選擇
這種選擇不存在正確與否,只有是否合適。因此這裡我們只是舉例說明一些合適的情況。
我們仍以上述程式碼為例,如果我們可以預見到以後需要把front_of_house模組和eat_at_restaurant函數移動到一個新的名為customer_experience的模組中,就應該使用相對路徑,這樣我們就對其進行調整。
類似的,如果我們需要把eat_at_restaurant函數移動到dining模組中,那麼我們選擇絕對路徑的話就不需要做調整。
綜上,我們需要對程式碼的優化方向有一些前瞻性,並以此來判斷需要使用相對路徑還是絕對路徑。
相對路徑除了以當前模組開頭外,還可以以super開頭。它表示的是父級模組,類似於文件系統中的兩個點(..
)。
use關鍵字
絕對路徑和相對路徑可以幫助我們找到指定的函數,但用起來也非常的麻煩,每次都要寫一大長串路徑。還好Rust為我們提供了use關鍵字。在很多語言中都有import關鍵字,這裡的use就有些類似於import。不過Rust會提供更加豐富的用法。
use最基本的用法就是引入一個路徑。我們就可以更加方便的使用這個路徑下的一些方法:
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); }
這個路徑可以是絕對路徑,也可以是相對路徑,但如果是相對路徑,就必須要以self開頭。上面的例子可以寫成:
use self::front_of_house::hosting;
這與我們前面講的相對路徑似乎有些矛盾,Rust官方說會在之後的版本處理這個問題。
use還可以更進一步,直接指向具體的函數或Struct或Enum。但習慣上我們使用函數時,use後面使用的是路徑,這樣可以在調用函數時知道它屬於哪個模組;而在使用Struct/Enum時,則具體指向它們。當然,這只是官方建議的編程習慣,你也可以有自己的習慣,不過最好還是按照官方推薦或者是項目約定的規範比較好。
對於同一路徑下的某些子模組,在引入時可以合併為一行,例如:
use std::io; use std::cmp::Ordering; // 等價於 use std::{cmp::Ordering, io};
有時我們還會遇到引用不同包下相同名稱Struct的情況,這時有兩種解決辦法,一是不指定到具體的Struct,在使用時加上不同的路徑;二是使用as
關鍵字,為Struct起一個別名。
方法一:
use std::fmt; use std::io; fn function1() -> fmt::Result { // --snip-- } fn function2() -> io::Result<()> { // --snip-- }
方法二:
use std::fmt::Result; use std::io::Result as IoResult; fn function1() -> Result { // --snip-- } fn function2() -> IoResult<()> { // --snip-- }
如果要導入某個路徑下的全部模組或函數,可以使用*
來表示。當然我是非常不建議使用這種方法的,因為導入全部的話,如果出現名稱衝突就會很難排查問題。
對於外部的依賴包,我們需要先在Cargo.toml文件中添加依賴,然後就可以在程式碼中使用use來引入依賴庫中的路徑。Rust提供了一些標準庫,即std下的庫。在使用這些標準庫時是不需要添加依賴的。
有些同學看到這裡可能要開始抱怨了,說好了介紹怎麼拆分文件,到現在還是在一個文件里玩,這不是欺騙讀者嘛。
別急,這就開始拆分。
開始拆分
我們拿剛才的一段程式碼為例
mod front_of_house { mod hosting { fn add_to_waitlist() {} fn seat_at_table() {} } mod serving { fn take_order() {} fn serve_order() {} fn take_payment() {} } }
首先我們可以把front_of_house模組下的內容拆分出去,需要在src目錄下新建一個front_of_house.rs文件,然後把front_of_house模組下的內容寫到文件中。lib.rs文件中,只需要聲明front_of_house模組即可,不需要具體的定義。聲明模組時,將花括弧即內容改為分號就可以了。
mod front_of_house;
然後我們可以繼續拆分front_of_house模組下的hosting模組和serving模組,這時需要新建一個名為front_of_house的文件件,在該文件夾下放置要拆分的模組的同名文件,把模組定義的內容寫在文件中,front_of_house.rs文件同樣只保留聲明即可。
拆分後的文件目錄如圖
本文主要講了Rust中Package、Crate、Module、Path的概念和用法,有了這些基礎,我們後面才有可能開發一些比較大的項目。
ps:本文的程式碼示例均來自the book。