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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容