Rust學習筆記之非常好用的包管理器Cargo

包管理器

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 new

可以看到,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 run

Cargo 還提供一個叫 cargo check 命令,可以快速檢查代碼並確保可以編譯,但不產生可執行文件。(通常 cargo checkcargo build 快得多,因爲它省略了可執行文件生成的步驟)

測試

測試是軟件開發中非常重要的一環,值得另外寫一篇來講,這裏只是簡單的提一下。新建一個庫的項目,作爲庫項目,並不打包成可執行文件,可以發現原本的 main.rs 被 lib.rs 取代了。lib.rs 裏面已經寫好了一個測試。
test

使用 cargo test 將運行這個這個測試。
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 運行。
examples

工作區

項目再大的時候,可能就要考慮把公共部分抽離出來作爲獨立的 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 appcargo 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 下運行這個項目。

workspace

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 進行檢查:
clippy
可以看到,這裏人家讓你簡化成 x 就行了。

小結

通過這篇,我們已經簡略地看到了 Cargo 如何初始化、構建、運行和測試代碼。 Cargo 還是非常好用,Visual Studio Code 裏安裝上 RLS 插件,寫起來的感覺很不錯。我第一次的時候就覺得這套工具設計得比golang人性化一點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章