當一個項目開始變得更大,把它分為一堆更小的部分然后再把它們裝配到一起被認為是一個好的軟件工程實踐。另外定義良好的接口也非常重要,這樣有些函數是私有的而有些是公有的。Rust有一個模塊系統來幫助我們處理這些工作。
## 基礎術語:包裝箱和模塊
Rust有兩個不同的術語與模塊系統有關:_包裝箱_(_crate_)和_模塊_(_module_)。包裝箱是其它語言中_庫_或_包_的同義詞。因此“Cargo”則是Rust包管理工具的名字:你通過Cargo發送你的包裝箱給別人。包裝箱可以根據項目的不同生成可執行文件或庫文件。
每個包裝箱有一個隱含的_根模塊_(_root module_)包含模塊的代碼。你可以在根模塊下定義一個子模塊樹。模塊允許你為自己模塊的代碼分區。
作為一個例子,讓我們來創建一個_短語_(_phrases_)包裝箱,它會給我們一些不同語言的短語。為了使事情變得簡單,我們僅限于“你好”和“再見”這兩個短語,并使用英語和日語的短語。我們采用如下模塊布局:
~~~
+-----------+
+---| greetings |
| +-----------+
+---------+ |
| english |---+
+---------+ | +-----------+
| +---| farewells |
+---------+ | +-----------+
| phrases |---+
+---------+ | +-----------+
| +---| greetings |
+----------+ | +-----------+
| japanese |---+
+----------+ |
| +-----------+
+---| farewells |
+-----------+
~~~
在這個例子中,`phrases`是我們包裝箱的名字。剩下的所有都是模塊。你可以看到它們組成了一個樹,它們以包裝箱為_根_,這同時也是樹的根:`phrases`。
現在我們有了一個計劃,讓我們在代碼中定義這些模塊。讓我們以用Cargo創建一個新包裝箱作為開始:
~~~
$ cargo new phrases
$ cd phrases
~~~
如果你還記得,這會為我們生成一個簡單的項目:
~~~
$ tree .
.
├── Cargo.toml
└── src
└── lib.rs
1 directory, 2 files
~~~
`src/lib.rs`是我們包裝箱的根,與上面圖表中的`phrases`對應。
## 定義模塊
我們用`mod`關鍵字來定義我們的每一個模塊。讓我們把`src/lib.rs`寫成這樣:
~~~
// in src/lib.rs
mod english {
mod greetings {
}
mod farewells {
}
}
mod japanese {
mod greetings {
}
mod farewells {
}
}
~~~
在`mod`關鍵字之后是模塊的名字。模塊的命名采用Rust其它標識符的命名慣例:`lower_snake_case`。在大括號中(`{}`)是模塊的內容。
在`mod`中,你可以定義子`mod`。我們可以用雙冒號(`::`)標記訪問子模塊。我們的4個嵌套模塊是`english::greetings`,`english::farewells`,`japanese::greetings`和`japanese::farewells`。因為子模塊位于父模塊的命名空間中,所以這些不會沖突:`english::greetings`和`japanese::greetings`是不同的,即便它們的名字都是`greetings`。
因為這個包裝箱的根文件叫做`lib.rs`,且沒有一個`main()`函數。Cargo會把這個包裝箱構建為一個庫:
~~~
$ cargo build
Compiling phrases v0.0.1 (file:///home/you/projects/phrases)
$ ls target
deps libphrases-a7448e02a0468eaa.rlib native
~~~
`libphrase-hash.rlib`是構建好的包裝箱。在我們了解如何使用這個包裝箱之前,先讓我們把它拆分為多個文件。
## 多文件包裝箱
如果每個包裝箱只能有一個文件,這些文件將會變得非常龐大。把包裝箱分散到多個文件也非常簡單,Rust支持兩種方法。
除了這樣定義一個模塊外:
~~~
mod english {
// contents of our module go here
}
~~~
我們還可以這樣定義:
~~~
mod english;
~~~
如果我們這么做的話,Rust會期望能找到一個包含我們模塊內容的`english.rs`文件,或者包含我們模塊內容的`english/mod.rs`文件:
注意在這些文件中,你不需要重新定義這些文件:它們已經由最開始的`mod`定義。
使用這兩個技巧,我們可以將我們的包裝箱拆分為兩個目錄和七個文件:
~~~
$ tree .
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── english
│ │ ├── farewells.rs
│ │ ├── greetings.rs
│ │ └── mod.rs
│ ├── japanese
│ │ ├── farewells.rs
│ │ ├── greetings.rs
│ │ └── mod.rs
│ └── lib.rs
└── target
└── debug
├── build
├── deps
├── examples
├── libphrases-a7448e02a0468eaa.rlib
└── native
~~~
`src/lib.rs`是我們包裝箱的根,它看起來像這樣:
~~~
mod english;
mod japanese;
~~~
這兩個定義告訴Rust去尋找`src/english.rs`和`src/japanese.rs`,或者`src/english/mod.rs`和`src/japanese/mod.rs`,具體根據你的偏好。在我們的例子中,因為我們的模塊含有子模塊,所以我們選擇第二種方式。`src/english/mod.rs`和`src/japanese/mod.rs`都看起來像這樣:
~~~
mod greetings;
mod farewells;
~~~
再一次,這些定義告訴Rust去尋找`src/english/greetings.rs`和`src/japanese/greetings.rs`,或者`src/english/farewells/mod.rs`和`src/japanese/farewells/mod.rs`。因為這些子模塊沒有自己的子模塊,我們選擇`src/english/greetings.rs`和`src/japanese/farewells.rs`。
現在`src/english/greetings.rs`和`src/japanese/farewells.rs`都是空的。讓我們添加一些函數。
在`src/english/greetings.rs`添加如下:
~~~
fn hello() -> String {
"Hello!".to_string()
}
~~~
在`src/english/farewells.rs`添加如下:
~~~
fn goodbye() -> String {
"Goodbye.".to_string()
}
~~~
在`src/japanese/greetings.rs`添加如下:
~~~
fn hello() -> String {
"こんにちは".to_string()
}
~~~
當然,你可以從本文復制粘貼這些內容,或者寫點別的東西。事實上你寫進去“konnichiwa”對我們學習模塊系統并不重要。
在`src/japanese/farewells.rs`添加如下:
~~~
fn goodbye() -> String {
"さようなら".to_string()
}
~~~
(這是“Sayōnara”,如果你很好奇的話。)
現在我們在包裝箱中添加了一些函數,讓我們嘗試在別的包裝箱中使用它。
## 導入外部的包裝箱
我們有了一個庫包裝箱。讓我們創建一個可執行的包裝箱來導入和使用我們的庫。
創建一個`src/main.rs`文件然后寫入如下:(現在它還不能編譯)
~~~
extern crate phrases;
fn main() {
println!("Hello in English: {}", phrases::english::greetings::hello());
println!("Goodbye in English: {}", phrases::english::farewells::goodbye());
println!("Hello in Japanese: {}", phrases::japanese::greetings::hello());
println!("Goodbye in Japanese: {}", phrases::japanese::farewells::goodbye());
}
~~~
`extern crate`聲明高速Rust我們需要編譯和鏈接`phrases`包裝箱。然后我們就可以在這里使用`phrases`的模塊了。就想我們之前提到的,你可以用雙冒號引用子模塊和之中的函數。
另外,Cargo假設`src/main.rs`是二進制包裝箱的根,而不是庫包裝箱的。現在我們的包中有兩個包裝箱:`src/lib.rs`和`src/main.rs`。這種模式在可執行包裝箱中非常常見:大部分功能都在庫包裝箱中,而可執行包裝箱使用這個庫。這樣,其它程序可以只使用我們的庫,另外這也是各司其職的良好分離。
現在它還不能很好的工作。我們會得到4個錯誤,它們看起來像:
~~~
$ cargo build
Compiling phrases v0.0.1 (file:///home/you/projects/phrases)
/home/you/projects/phrases/src/main.rs:4:38: 4:72 error: function `hello` is private
/home/you/projects/phrases/src/main.rs:4 println!("Hello in English: {}", phr
ases::english::greetings::hello());
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
note: in expansion of format_args!
:2:23: 2:77 note: expansion site
:1:1: 3:2 note: in expansion of println!
/home/you/projects/phrases/src/main.rs:4:5: 4:76 note: expansion site
~~~
Rust的一切默認都是私有的。讓我們深入了解一下這個。
## 導出公用接口
Rsut允許你嚴格的控制你的接口哪部分是公有的,所以它們默認都是私有的。你需要使用`pub`關鍵字,來公開它。讓我們先關注`english`模塊,所以讓我們像這樣減少`src/main.rs`的內容:
~~~
extern crate phrases;
fn main() {
println!("Hello in English: {}", phrases::english::greetings::hello());
println!("Goodbye in English: {}", phrases::english::farewells::goodbye());
}
~~~
在我們的`src/lib.rs`,讓我們給`english`模塊聲明添加一個`pub`:
~~~
pub mod english;
mod japanese;
~~~
然后在我們的`src/english/mod.rs`中,加上兩個`pub`:
~~~
pub mod greetings;
pub mod farewells;
~~~
在我們的`src/english/greetings.rs`中,讓我們在`fn`聲明中加上`pub`:
~~~
pub fn hello() -> String {
"Hello!".to_string()
}
~~~
然后在`src/english/farewells.rs`中:
~~~
pub fn goodbye() -> String {
"Goodbye.".to_string()
}
~~~
這樣,我們的包裝箱就可以編譯了,雖然會有警告說我們沒有使用`japanese`的方法:
~~~
$ cargo run
Compiling phrases v0.0.1 (file:///home/you/projects/phrases)
/home/you/projects/phrases/src/japanese/greetings.rs:1:1: 3:2 warning: code is never used: `hello`,
#[warn(dead_code)] on by default
/home/you/projects/phrases/src/japanese/greetings.rs:1 fn hello() -> String {
/home/you/projects/phrases/src/japanese/greetings.rs:2 "こんにちは".to_string()
/home/you/projects/phrases/src/japanese/greetings.rs:3 }
/home/you/projects/phrases/src/japanese/farewells.rs:1:1: 3:2 warning: code is never used: `goodbye`,
#[warn(dead_code)] on by default
/home/you/projects/phrases/src/japanese/farewells.rs:1 fn goodbye() -> String {
/home/you/projects/phrases/src/japanese/farewells.rs:2 "さようなら".to_string()
/home/you/projects/phrases/src/japanese/farewells.rs:3 }
Running `target/phrases`
Hello in English: Hello!
Goodbye in English: Goodbye.
~~~
現在我們的函數是公有的了,我們可以使用它們。好的!然而,`phrases::english::greetings::hello()`非常長并且重復。Rust有另一個關鍵字用來導入名字到當前空間中,這樣我們就可以用更短的名字來引用它們。讓我們聊聊`use`。
## 用`use`導入模塊
Rust有一個`use`關鍵字,它允許我們導入名字到我們本地的作用域中。讓我們把`src/main.rs`改成這樣:
~~~
extern crate phrases;
use phrases::english::greetings;
use phrases::english::farewells;
fn main() {
println!("Hello in English: {}", greetings::hello());
println!("Goodbye in English: {}", farewells::goodbye());
}
~~~
這兩行`use`導入了兩個模塊到我們本地作用域中,這樣我們就可以用一個短得多的名字來引用函數。作為一個傳統,當導入函數時,導入模塊而不是直接導入函數被認為是一個最佳實踐。也就是說,你可以這么做:
~~~
extern crate phrases;
use phrases::english::greetings::hello;
use phrases::english::farewells::goodbye;
fn main() {
println!("Hello in English: {}", hello());
println!("Goodbye in English: {}", goodbye());
}
~~~
不過這并不理想。這意味著更加容易導致命名沖突。在我們的小程序中,這沒什么大不了的,不過隨著我們的程序增長,它將會成為一個問題。如果我們有命名沖突,Rust會給我們一個編譯錯誤。舉例來說,如果我們將`japanese`的函數設為公有,然后這樣嘗試:
~~~
extern crate phrases;
use phrases::english::greetings::hello;
use phrases::japanese::greetings::hello;
fn main() {
println!("Hello in English: {}", hello());
println!("Hello in Japanese: {}", hello());
}
~~~
Rust會給我們一個編譯時錯誤:
~~~
Compiling phrases v0.0.1 (file:///home/you/projects/phrases)
/home/you/projects/phrases/src/main.rs:4:5: 4:40 error: a value named `hello`
has already been imported in this module
/home/you/projects/phrases/src/main.rs:4 use phrases::japanese::greetings::hello;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `phrases`.
~~~
如果你從同樣的模塊中導入多個名字,我們不必寫多遍。Rust有一個簡便的語法:
~~~
use phrases::english::greetings;
use phrases::english::farewells;
~~~
你可以使用大括號:
~~~
use phrases::english::{greetings, farewells};
~~~
這兩種聲明是等同的,不過第二種少打更多字。
## 使用`pub use`重導出
你不僅可以用`use`來簡化標識符。你也可以在包裝箱內用它重導出函數到另一個模塊中。這意味著你可以展示一個外部接口可能并不直接映射到內部代碼結構。
讓我們看個例子。修改`src/main.rs`讓它看起來像這樣:
~~~
extern crate phrases;
use phrases::english::{greetings,farewells};
use phrases::japanese;
fn main() {
println!("Hello in English: {}", greetings::hello());
println!("Goodbye in English: {}", farewells::goodbye());
println!("Hello in Japanese: {}", japanese::hello());
println!("Goodbye in Japanese: {}", japanese::goodbye());
}
~~~
然后修改`src/lib.rs`公開`japanese`模塊:
~~~
pub mod english;
pub mod japanese;
~~~
接下來,把這兩個函數聲明為公有,先是`src/japanese/greetings.rs`:
~~~
pub fn hello() -> String {
"こんにちは".to_string()
}
~~~
然后是`src/japanese/farewells.rs`:
~~~
pub fn goodbye() -> String {
"さようなら".to_string()
}
~~~
最后,修改你的`src/japanese/mod.rs`為這樣:
~~~
pub use self::greetings::hello;
pub use self::farewells::goodbye;
mod greetings;
mod farewells;
~~~
`pub use`聲明將這些函數導入到了我們模塊結構空間中。因為我們在`japanese`模塊內使用了`pub use`,我們現在有了`phrases::japanese::hello()`和`phrases::japanese::goodbye()`函數,即使它們的代碼在`phrases::japanese::greetings::hello()`和`phrases::japanese::farewells::goodbye()`函數中。內部結構并不反映外部接口。
這里我們對每個我們想導入到`japanese`空間的函數使用了`pub use`。我們也可以使用通配符來導入`greetings`的一切到當前空間中:`pub use self::greetings::*`。
那么`self`怎么辦呢?好吧,默認,`use`聲明是絕對路徑,從你的包裝箱根開始。`self`則使路徑相對于你在結構中的當前位置。這里有一個更特殊的`use`形式:你可以使用`use super::`來到達你樹中當前位置的上一級。一些同學喜歡把`self`看作`.`而把`super`看作`..`,它們在許多shell表示為當前目錄和父目錄。
除了`use之`外,路徑是相對的:`foo::bar()`引用一個相對我們位置的`foo`中的函數。如果它帶有`::`前綴,它引用了一個不同的`foo`,一個從你包裝箱根開始的絕對路徑。
另外,注意`pub use`出現在`mod`定義之前。Rust要求`use`位于最開始。
構建然后運行:
~~~
$ cargo run
Compiling phrases v0.0.1 (file:///home/you/projects/phrases)
Running `target/phrases`
Hello in English: Hello!
Goodbye in English: Goodbye.
Hello in Japanese: こんにちは
Goodbye in Japanese: さようなら
~~~
- 前言
- 1.介紹
- 2.準備
- 2.1.安裝Rust
- 2.2.Hello, world!
- 2.3.Hello, Cargo!
- 3.學習Rust
- 3.1.猜猜看
- 3.2.哲學家就餐問題
- 3.3.其它語言中的Rust
- 4.高效Rust
- 4.1.棧和堆
- 4.2.測試
- 4.3.條件編譯
- 4.4.文檔
- 4.5.迭代器
- 4.6.并發
- 4.7.錯誤處理
- 4.8.外部語言接口
- 4.9.Borrow 和 AsRef
- 4.10.發布途徑
- 5.語法和語義
- 5.1.變量綁定
- 5.2.函數
- 5.3.原生類型
- 5.4.注釋
- 5.5.If語句
- 5.6.for循環
- 5.7.while循環
- 5.8.所有權
- 5.9.引用和借用
- 5.10.生命周期
- 5.11.可變性
- 5.12.結構體
- 5.13.枚舉
- 5.14.匹配
- 5.15.模式
- 5.16.方法語法
- 5.17.Vectors
- 5.18.字符串
- 5.19.泛型
- 5.20.Traits
- 5.21.Drop
- 5.22.if let
- 5.23.trait對象
- 5.24.閉包
- 5.25.通用函數調用語法
- 5.26.包裝箱和模塊
- 5.27.`const`和`static`
- 5.28.屬性
- 5.29.`type`別名
- 5.30.類型轉換
- 5.31.關聯類型
- 5.32.不定長類型
- 5.33.運算符和重載
- 5.34.`Deref`強制多態
- 5.35.宏
- 5.36.裸指針
- 6.Rust開發版
- 6.1.編譯器插件
- 6.2.內聯匯編
- 6.3.不使用標準庫
- 6.4.固有功能
- 6.5.語言項
- 6.6.鏈接參數
- 6.7.基準測試
- 6.8.裝箱語法和模式
- 6.9.切片模式
- 6.10.關聯常量
- 7.詞匯表
- 8.學院派研究
- 勘誤