包管理器
Rust 的 Cargo 應該算是衆多包管理器當中非常好用的一個。如果接觸過前端開發,對 npm/yarn 應該是不陌生的,Go 語言也有 go tool。這些包管理器用來下載正確的依賴庫、編譯和鏈接文件,還有管理項目等功能。
C++ 沒有一個專用的包管理。C/C++ 一般用 GNU make 來構建項目。GNU make 是一個與語言無關的構建工具。GNU make 非常原始,既沒有提供頭文件的查找的功能,必須手動指明目錄,也無法自動地進行依賴的下載。
幸運的是,Rust 的包管理器 Cargo 解決了這些問題,是一個非常好用的包管理工具。
模塊(Modules)
任何 Rust 項目都有一個根模塊。如果創建的是一個庫,根模塊就是 lib.rs 文件。如果是一個可執行的應用,那麼根模塊就是有 main
函數的文件。
嵌套模塊
最簡單的創建模塊的方法就是 mod{}
。
// mod_within.rs
mod food {
struct Cake;
struct Smoothie;
struct Pizza;
}
fn main() {
let eatable = Cake;
}
如果這個時候進行編譯,會發現報錯了。
Rust 真的這點上很贊,不單單是報錯,連參考的解決方案都寫好了。我們按照編譯器提示的那樣,增加一行代碼。
// mod_within.rs
mod food {
struct Cake;
struct Smoothie;
struct Pizza;
}
use food::Cake;
fn main() {
let eatable = Cake;
}
再編譯一下,結果發現…
現在編譯器說,Cake
是私有的。現在再修改一下代碼,在 Cake
的聲明前加上 pub
:
// mod_within.rs
mod food {
pub struct Cake;
struct Smoothie;
struct Pizza;
}
use food::Cake;
fn main() {
let eatable = Cake;
}
現在可以編譯了。可以發現總共就兩步,一步公開,一步引入。沒有公開 pub
,默認是私有的,外部是不能調用的。
文件模塊
模塊也可以是一個文件,把它放在與 mian.rs 同一文件夾下,然後在 main.rs 中引入該模塊。我們創建一個文件夾,順便創建兩個文件。
控制檯命令如下(用鼠標完成下面的工作也是一個不錯的選擇):
mkdir modules_demo && cd modules_demo
touch foo.rs && touch main.rs
tree .
.
├── foo.rs
└── main.rs
接下來,在創建好的 foo.rs 中提供一個結構體,並實現一個方法。注意一下,pub
的使用。
// modules_demo/foo.rs
pub struct Bar;
impl Bar {
pub fn init() {
println!("Bar type initialized");
}
}
接下來,在 main.rs
中使用這個模塊。
// modules_demo/main.rs
mod foo;
use crate::foo::Bar;
fn main() {
let _bar = Bar::init();
}
我們定義模塊,foo
,然後通過 mod foo
引入。之後,使用 crate::foo::Bar
。注意到這個前綴 crate
。
絕對導入:
- crate:一個絕對導入的前綴指向當前的根 crate 。在前面這個代碼裏,根模塊就是 main.rs。
相對導入:
- self:從當前的模塊中相對引入。大多數時候被用來再引入父模塊中的子模塊。
- super:可以從父模塊中引入對應的項。比如說測試模塊可能就要引入父模塊。
crate 可以理解爲項目,是一個完整的編譯單元;crate 內部則使用 mod,這個類似C++的語言的命名空間 namespace。
文件夾模塊
還可以創建文件夾來表示一個模塊。這種方式下還可以繼續創建子文件夾或文件實現層次關係。我們在上述基礎上創建一個文件夾 foo,文件夾結構如下:
modules_demo
├── foo
│ └── bar.rs
├── foo.rs
└── main.rs
在 bar.rs 中加入下面的代碼:
// foo/bar.rs
pub struct Bar;
impl Bar {
pub fn hello() {
println!("Hello from Bar!");
}
}
這裏聲明瞭一個單元結構體(unit struct)聯繫到了 hello
上。我們會在 main.rs 中使用這個API。接下來,將 foo.rs 修改一下:
// modules_demo/foo.rs
mod bar;
pub use self::bar::Bar;
pub fn do_foo() {
println!("Hi from foo!");
}
我們使用了 mod
聲明瞭模塊 bar
。接下來,從模塊 bar 中重新導出 Bar
。重新導出適合導入隱藏在嵌套子模塊中的項。可以看到,pub use
指明重新導出的 Bar
並不是在這裏實現的。pub use
就是把其它地方的元素當作模塊的直接成員公開出去。有了這個機制,就可以輕鬆做好接口和實現的分離。
最後,在 main.rs 中使用:
// modules_demo/main.rs
mod foo;
use foo::Bar;
fn main() {
foo::do_foo();
Bar::hello();
}
// Hi from foo!
// Hello from Bar!
Cargo 和 crate
項目越來越大的時候,通常的做法就是把代碼重構成更小,更容易管理的模塊或者庫。完善的文檔,構建方式,依賴管理也會變得非常的重要。Cargo 就是用來處理這些事情的,https://crates.io 託管着註冊的 Rust 庫,你可以找到一些有用的第三方庫來加速開發進程。
通常來說,crate 可以來自於本地的文件夾、Git倉庫(比如Github)、或者 crates.io。Cargo 對這些來源都給予了支持。
創建新項目
Cargo 默認會創建一個二進制項目。加上 --lib
則會生成一個庫(library)項目。接下來,創建一個 imgtool 來看看 crate 的結構。
cargo new imgtool
可以看到,Cargo 創建了1個文件夾,總共兩個文件。與此同時,Cargo 其實還創建了 git 倉庫,還有一個隱藏文件 .gitignore,裏面已經寫着 /target,會自動把生成的二進制文件過濾掉,不進入git倉庫。
目前主流的VCS(Version Control System,版本控制系統)主要就是git,但是有用戶使用的是 hg(mercurial),還有比如pijul(用Rust寫的)、fossil。要改變版本控制系統,只需要傳入 --vcs
加上相應的工具即可。
main.rs
中已經寫好了打印 Hello, world! 的樣例;接下來,看看 Cargo.toml:
[package]
name = "imgtool"
version = "0.1.0"
authors = ["testuser <[email protected]>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
基本的作者和項目信息,下面還有一個依賴。edition 主要是2015和2018,創建項目的時候可以通過--edition
指定。不過,現在創建項目肯定是要使用2018啦,所以默認就行了。順便提一下,Toml 配置文件並不是 Rust 獨有的,是由 Tom Preston-Werner 創建的標準。
依賴
Cargo 管理依賴主要是兩個文件,一個是就是剛剛看到的 Cargo.toml。如果添加依賴後,還會有一個 Cargo.lock。Cargo.toml 確定了需要那些依賴包,由於依賴包可能會隨着版本的更新而變得不兼容。所以,需要使用 Cargo.lock 明確地鎖定版本,防止日後構造的時候在依賴包版本上出問題。
依賴包的更新可以使用 cargo update
命令;如果只是想更新某一個包,可以用 cargo update -p <crate-name>
指定包的名稱。
Cargo 遵循語義版本規則,版本號由三個部分構成 主版本號.次版本號.修訂號:
- 主版本號:有不兼容的API的修改;
- 次版本號:向下兼容的功能性新增;
- 修訂號:向下兼容的問題修正;
構建
cargo build
會創建可執行文件在 target/debug/hello_cargo
中。debug 的版本會加入一些用於調試的工具,並不是性能最好的。實際發佈的時候,應該使用 --release
參數構建發佈(release)版本。
也可以使用 cargo run
在一個命令中同時編譯並運行生成的可執行文件。當然,也可以使用 cargo run --release
運行發佈版本。
Cargo 還提供一個叫 cargo check
命令,可以快速檢查代碼並確保可以編譯,但不產生可執行文件。(通常 cargo check
比 cargo build
快得多,因爲它省略了可執行文件生成的步驟)
測試
測試是軟件開發中非常重要的一環,值得另外寫一篇來講,這裏只是簡單的提一下。新建一個庫的項目,作爲庫項目,並不打包成可執行文件,可以發現原本的 main.rs 被 lib.rs 取代了。lib.rs 裏面已經寫好了一個測試。
使用 cargo test
將運行這個這個測試。
我們嘗試使用 TDD (Test Driven Development,測試驅動開發)的開發理念來開發一個初級版本的指數函數。簡單來講,先寫測試,然後運行測試(必定失敗),再寫代碼通過這個測試,接着繼續寫一個測試,運行測試失敗,再寫代碼…構成一個測試—代碼—測試 的閉環。通過不斷地編寫測試,實現一種增量的開發。
// src/lib.rs
pub fn pow(base: i64, exponent: usize) -> i64 {
unimplemented!();
}
#[cfg(test)]
mod tests {
use super::pow;
#[test]
fn minus_two_raised_three_is_minus_eight() {
assert_eq!(pow(-2, 3), -8);
}
}
這裏我們完全沒有編寫函數功能,但寫了第一個測試,如果函數參數是 -2和3,期望結果是-8。現在運行測試,顯然失敗。
接下來,修復這個測試:
// src/lib.rs
pub fn pow(base: i64, exponent: usize) -> i64 {
let mut res = 1;
if exponent == 0 {
return 1;
}
for _ in 0..exponent {
res *= base as i64;
}
res
}
好了測試應該可以正常運行了。爲了讓用戶快速上手,Cargo 實踐中建議增加 examples 文件夾,裏面可以放一個或多個含 main
函數的文件,用來展示用法。因此新建一個examples目錄,並添加basic.rs 文件:
use myexponent::pow;
fn main() {
println!("8 raised to 2 is {}", pow(8, 2));
}
可以通過 cargo run --example basic
運行。
工作區
項目再大的時候,可能就要考慮把公共部分抽離出來作爲獨立的 Crate 去管理這個複雜的應用。工作區(workspace)的概念就是你可以把 crate 放在文件夾,然後共享 Cargo.toml。
新建一個文件夾,
mkdir workspace_demo
cd workspace_demo && touch cargo.toml
之後在 cargo.toml 中寫入:
# workspace_demo/Cargo.toml
[workspace]
members = ["my_crate", "app"]
members
就是在 workspace 之下的 crate 列表。接下來使用 cargo new app
和 cargo new my_crate --lib
創建兩個 crate。然後在 my_crate 中添加一個方法:
// workspace_demo/my_crate/src/lib.rs
pub fn greet() {
println!("Hi from my_crate");
}
接着在 app 裏使用這個方法:
// workspace_demo/app/src/main.rs
fn main() {
my_crate::greet();
}
我們需要讓 Cargo 知道 my_crate 依賴。my_crate 作爲本地的crate,需要特別在 Cargo.toml 指定依賴路徑:
# workspace_demo/app/Cargo.toml
# ...
[dependencies]
my_crate = { path = "../my_crate" }
接下來,就可以在 workspace_demo 下運行這個項目。
Cargo 工具和命令
- cargo-watch:前面提到了
cargo check
適合快速檢查;可以用cargo install cargo-watch
,只要代碼改變,自動運行 cargo check。 - cargo-edit:自動化地添加依賴到 Cargo.toml。
- cargo-deb:便於創建 Debian Linux 下 deb 包
- cargo-outdated:顯示過時的包
clippy
靜態檢查通過一些實踐可以讓代碼保持高質量。可以使用 rustup component add clippy
添加 clippy。
比如在前面 myexponent 的 src/lib.rs中,添加兩行可以進行優化的代碼:
// src/lib.rs
pub fn pow(base: i64, exponent: usize) -> i64 {
//////// Dummy code for clippy demo
let x = true;
if x == true {}
/////////
let mut res = 1;
if exponent == 0 {
return 1;
}
for _ in 0..exponent {
res *= base as i64;
}
res
}
// . ..
使用 cargo clippy 進行檢查:
可以看到,這裏人家讓你簡化成 x
就行了。
小結
通過這篇,我們已經簡略地看到了 Cargo 如何初始化、構建、運行和測試代碼。 Cargo 還是非常好用,Visual Studio Code 裏安裝上 RLS 插件,寫起來的感覺很不錯。我第一次的時候就覺得這套工具設計得比golang人性化一點。