The Rust programming language 讀書筆記——包(package)、單元包(crate)與模塊系統(tǒng)

模塊系統(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ù)定義其他模塊(如本例中的 hostingserving)被丧。模塊內(nèi)同樣也可以包含其他條目的定義,如結(jié)構(gòu)體绪妹、枚舉甥桂、常量、trait 或函數(shù)等邮旷。

src/main.rssrc/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

即模塊 hostingserving 是私有的鲸鹦,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)記 hostingserving 模塊侄旬。
需要注意的是,模塊被 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_househosting 節(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 語句闸迷,hostingserving 成了該作用域下的一個(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è)名稱會以私有的方式在新的作用域中生效铸史。為了讓外部代碼能夠訪問到這些名稱鼻疮,可以通過組合使用 pubuse 修飾其路徑。
這項(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)用依舊有效饼灿。

參考資料

The Rust Programming Language

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市厅克,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌橙依,老刑警劉巖证舟,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異窗骑,居然都是意外死亡女责,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門创译,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抵知,“玉大人,你說我怎么就攤上這事软族∷⑾玻” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵立砸,是天一觀的道長掖疮。 經(jīng)常有香客問我,道長颗祝,這世上最難降的妖魔是什么浊闪? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮螺戳,結(jié)果婚禮上搁宾,老公的妹妹穿的比我還像新娘。我一直安慰自己倔幼,他們只是感情好盖腿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著损同,像睡著了一般奸忽。 火紅的嫁衣襯著肌膚如雪堕伪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天栗菜,我揣著相機(jī)與錄音欠雌,去河邊找鬼。 笑死疙筹,一個(gè)胖子當(dāng)著我的面吹牛富俄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播而咆,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼霍比,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了暴备?” 一聲冷哼從身側(cè)響起悠瞬,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎涯捻,沒想到半個(gè)月后浅妆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡障癌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年凌外,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涛浙。...
    茶點(diǎn)故事閱讀 40,096評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡康辑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出轿亮,到底是詐尸還是另有隱情疮薇,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布我注,位于F島的核電站惦辛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏仓手。R本人自食惡果不足惜胖齐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嗽冒。 院中可真熱鬧呀伙,春花似錦、人聲如沸添坊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至雨女,卻和暖如春谚攒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背氛堕。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工馏臭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人讼稚。 一個(gè)月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓括儒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親锐想。 傳聞我的和親對象是個(gè)殘疾皇子帮寻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容