在開發 Rust 專案時,良好的檔案不僅是一種禮節,更是確保程式碼可維護性的關鍵。經過多年的 Rust 開發經驗,我發現檔案不僅是對程式的描述,更是程式碼設計思想的體現。

檔案即測試:rustdoc 的實用功能

rustdoc 提供了一個我特別欣賞的功能:檔案中的程式碼範例會被編譯並作為整合測試執行。這意味著你可以在檔案中包含帶有斷言的程式碼範例,這些範例會在執行 cargo test 時被測試。這確保了你的檔案包含有效與正確的程式碼範例。

以下是一個簡單的範例,我們可以將其附加到 src/lib.rs 的 crate 級別檔案中:

//! # 範例
//!
//! ```
//! use rustdoc_example::mult;
//! assert_eq!(mult(10, 10), 100);
//! ```

當我們使用 cargo test 執行測試時,這段檔案程式碼也會被測試:

cargo test
Compiling rustdoc-example v0.1.0
Finished test [unoptimized + debuginfo] target(s) in 0.42s
Running target/debug/deps/rustdoc_example-bec4912aee60500b
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;
finished in 0.00s
Doc-tests rustdoc-example
running 1 test
test src/lib.rs - (line 7) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;
finished in 0.23s

這段輸出展示了測試過程中發生的事情:首先執行了常規的單元測試(tests::it_works),然後執行了檔案測試。檔案測試成功透過,這意味著我們的檔案中的程式碼範例是有效的,並且與我們的函式實作相符。

rustdoc 語法快速參考

在使用 rustdoc 時,瞭解其語法是非常重要的:

語法描述
//!Crate 或模組級別的檔案,通常放在 crate 或模組的頂部。使用 CommonMark 語法。
///用於記錄後面的模組、函式、特性或類別。使用 CommonMark 語法。
[func], [func], [Foo](Bar)在檔案中連結到函式、模組或其他類別。關鍵字必須在作用域內才能正確連結。

對於想深入瞭解 rustdoc 的開發者,建議查閱官方檔案。對於 CommonMark 語法,我發現它的學習曲線相當平緩,大多數開發者能夠快速掌握。

Rust 模組系統:程式碼的組織藝術

在我開發較大型 Rust 專案時,合理的程式碼組織方式是成功的關鍵。Rust 的模組系統提供了一種階層化組織程式碼的方法,將程式碼分割成一組不同的單元,這些單元可以選擇性地分割成不同的源檔案。

模組的基本概念

Rust 的模組結合了兩個功能:從其他源檔案包含程式碼,以及對公開可見符號的名稱空間管理。在 Rust 中,所有符號預設都是私有的,但可以使用 pub 關鍵字將其匯出(或使其公開可見)。

模組宣告使用 mod 關鍵字,可選的 pub 可見性指定符,後面立即跟著大括號中的程式碼區塊:

mod private_mod {
    // 私有程式碼放在這裡
}

pub mod public_mod {
    // 要匯出的公共程式碼放在這裡
}

在 Rust 社群中,模組(module)和 mod 有時會被交替使用。按照慣例,模組名稱通常使用蛇形命名法(snake_case),而大多數其他名稱使用駝峰式命名法(camelCase)。原始類別(如 i32stru64 等)通常是短的單詞,有時使用蛇形命名法。常數通常使用大寫字母,這也是大多數其他語言的慣例。

我們也可以使用相同的 mod 關鍵字包含模組,但不是程式碼區塊,而是以分號結束:

mod private_mod;
pub mod public_mod;

模組的可見性

在 Rust 中,關於可見性,預設情況下所有內容都是私有的,除了公共特性(traits)和公共列舉(enums),其中關聯項預設是公共的。私有作用域的宣告繫結到一個模組,這意味著它們可以從宣告的模組(和子模組)內部存取。

使用 pub 關鍵字將可見性改為公共,並帶有可選的修飾符:我們可以使用 pub(modifier)crateselfsuper 或帶有指向另一個模組路徑的 in path。例如,pub(crate) 指定一個項在 crate 內是公共的,但在 crate 外不可存取。

在模組內宣告的項不會在 crate 範圍外匯出,除非模組本身也是公共的。例如,在以下程式碼中,我們有兩個公共函式,但在這種情況下,只有 public_mod_fn() 在 crate 外可見:

mod private_mod {
    pub fn private_mod_fn() {}
}

pub mod public_mod {
    pub fn public_mod_fn() {}
}

在這個範例中,雖然 private_mod_fn() 被標記為 pub,但由於它所在的模組 private_mod 是私有的,所以該函式在 crate 外部仍然不可見。只有 public_mod_fn() 既位於公共模組中又被宣告為公共函式,因此可以從 crate 外部存取。

此外,公共模組內的私有範圍項仍然是私有的,不能在其 crate 外部存取。Rust 的可見性相當直觀,並有助於防止意外的抽象洩漏。

模組的巢狀結構

模組可以深度巢狀:

mod outer_mod {
    mod inner_mod {
        mod super_inner_mod {
            // ...
        }
    }
}

當我們從另一個 crate 包含一個符號或模組時,我們使用 use 陳述式:

use serde::ser::{Serialize, Serializer};

這行程式碼從 serde crate 的 ser 模組中包含了 Serialize 和 Serializer 符號。當我們使用 use 陳述式包含程式碼時,第一個名稱通常是我們想要包含程式碼的 crate 名稱,後面跟著模組、特定符號或萬用字元(*)以包含該模組中的所有符號。

根據檔案系統的模組織

模組也可以使用檔案系統組織。我們可以使用 crate 的源目錄中的路徑建立與前面例子相同的層次結構,但我們仍然需要告訴 cargo 將哪些檔案包含在 crate 中。為此,我們使用 mod 陳述式而不是區塊。

考慮一個具有以下結構的 crate:

.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── lib.rs
    ├── outer_module
    │   └── inner_module
    │       ├── mod.rs
    │       └── super_inner_module.rs
    └── outer_module.rs

在我們的頂層 lib.rs 中,我們將包含外部模組,該模組在 outer_module.rs 中定義:

// lib.rs
mod outer_module;

編譯器將在 outer_module.rs 或 outer_module/mod.rs 中查詢 mod 宣告。在我們的例子中,我們在與 lib.rs 相同級別提供了 outer_module.rs。

在 outer_module.rs 中,我們有以下內容來包含內部模組:

// outer_module.rs
mod inner_module;

編譯器接下來會在外部模組中查詢 inner_module.rs 或 inner_module/mod.rs。在這種情況下,它找到了 inner_module/mod.rs,其中包含以下內容:

// inner_module/mod.rs
mod super_inner_module;

這包括了 inner_module 目錄中的 super_inner_module.rs。

這個例子展示了 Rust 如何透過檔案系統組織模組。雖然比直接在一個檔案中定義所有模組看起來更複雜,但對於較大的專案來說,使用模組比將所有原始碼都包含在 lib.rs 或 main.rs 中要好得多。如果模組看起來有點混亂,嘗試從頭開始重新建立類別的結構,以瞭解各個部分如何配合在一起。

透過這種方式,我們可以將相關的功能組織在一起,使程式碼更容易維護和理解。在實際開發中,我通常會根據功能或領域來組織模組,這樣團隊成員可以更容易地找到他們需要的程式碼。

Cargo 工作空間:管理大型專案

在開發大型 Rust 專案時,Cargo 的工作空間功能允許你將一個大型 crate 拆分為多個單獨的 crate,並將這些 crate 組合在一個分享單一 Cargo.lock 鎖設定檔的工作空間中。

工作空間的主要功能

工作空間有一些重要的功能,但它們的主要特點是允許你分享 Cargo.toml 中的引數和單一 Cargo.lock 中的已解析依賴樹。工作空間內的每個專案分享以下內容:

  • 頂層的 Cargo.lock 檔案
  • target/ 輸出目錄,包含所有工作空間的專案目標
  • 頂層 Cargo.toml 中的 [patch]、[replace] 和 [profile.*] 部分

使用工作空間

要使用工作空間,你需要像平常一樣使用 Cargo 在子目錄中建立專案,這些子目錄不應該與頂層 crate 的目錄重疊(即它們不應該在 src/、target/、tests/、examples/、benches/ 等中)。然後,你可以像平常一樣增加這些依賴,不過不是指定版本或儲存函式庫是簡單地指定路徑或將每個專案增加到工作空間中。

工作空間是管理大型 Rust 專案的理想方式,特別是當專案由多個相互關聯但功能獨立的元件組成時。透過分享 Cargo.lock 檔案,工作空間確保所有元件使用相同版本的依賴,這有助於避免版本不比對問題。

在我的專案中,我經常使用工作空間來組織核心功能、API 層和各種輔助工具。這種方法使得每個部分都可以獨立開發和測試,同時保持整體專案的一致性。

寫好 Rust 程式碼的實用技巧

在使用 Rust 進行開發的過程中,我發現了一些實用的技巧,可以幫助寫出更好的程式碼:

檔案優先開發

在開始編寫實際程式碼之前,先寫檔案可以幫助你理清思路。使用 rustdoc 的測試功能,你可以確保你的檔案和程式碼保持同步。

模組的明智設計

設計模組結構時,考慮以下幾點:

  1. 按功能或領域組織模組,而不是按類別
  2. 保持模組結構相對扁平,避免過深的巢狀
  3. 謹慎使用可見性修飾符,只公開真正需要被外部使用的項

工作空間的有效利用

對於較大的

Rust 工作區:多元化專案的組織術

在開發大型 Rust 應用時,專案結構管理往往會成為一個關鍵挑戰。隨著專案規模增長,將程式碼分割成多個相互關聯的模組變得日益重要。Cargo 的工作區(Workspaces)功能正是為解決這個問題而設計的強大工具。

工作區的本質與價值

工作區允許我們在單一專案中管理多個相關的子專案,這些子專案可以分享相同的構建輸出目錄與依賴關係。這種結構不僅使專案組織更加清晰,還能大幅提升構建效率。

在實際開發中,我發現工作區特別適合以下幾種情境:

  • 微服務架構的應用,每個服務可以是一個子專案
  • 共用核心邏輯但有多種前端介面的應用
  • 需要將功能拆分成多個可獨立發布的套件的專案

工作區例項:從零開始

讓我們實際構建一個使用工作區的專案,看它如何運作。首先,建立一個頂層應用:

$ cargo new workspaces-example
Created binary (application) `workspaces-example` package
$ cd workspaces-example

接著,建立一個子專案,這裡我們將其設為一個函式庫

$ cargo new subproject --lib

現在的目錄結構應該如下:

.
├── Cargo.toml
├── src
│   └── main.rs
└── subproject
    ├── Cargo.toml
    └── src
        └── lib.rs

這個目錄結構展示了一個典型的 Rust 工作區起始狀態。頂層專案有自己的 Cargo.tomlmain.rs,而子專案 subproject 則包含一個獨立的 Cargo.tomllib.rs。這種結構使得每個子專案都能獨立開發,同時又能在頂層專案中被統一管理。

設定工作區

要讓頂層專案認識到子專案,我們需要更新頂層的 Cargo.toml 檔案,增加子專案作為依賴並設定工作區:

[dependencies]
subproject = { path = "./subproject" }

[workspace]
members = ["subproject"]

這段設定做了兩件關鍵的事情:

  1. [dependencies] 中,我們指定 subproject 作為依賴,並透過 path 屬性指向本地子專案目錄
  2. [workspace] 中,我們將 subproject 增加到 members 陣列中,這樣 Cargo 就會將其識別為工作區的成員

對於較大型的專案,我們也可以使用萬用字元(glob pattern)來指定工作區成員,例如 members = ["components/*"],這樣就不需要手動列出每個子專案了。

實作子專案功能

接下來,讓我們在子專案中實作一個簡單的功能,然後在頂層應用中使用它。首先,編輯 subproject/src/lib.rs

pub fn hello_world() -> String {
    String::from("Hello, world!")
}

然後更新頂層應用的 src/main.rs

fn main() {
    println!("{}", subproject::hello_world());
}

現在執行專案:

$ cargo run
Compiling subproject v0.1.0
Compiling workspaces-example v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 0.85s
Running `target/debug/workspaces-example`
Hello, world!

這個範例展示了工作區的核心優勢之一:在頂層應用中直接使用子專案的功能。注意 main.rs 中我們可以直接透過 subproject::hello_world() 來呼叫子專案中定義的函式,就像使用外部套件一樣簡單。

工作區的進階應用

在實際開發中,工作區還有一些進階用法值得探索:

  1. 虛擬清單(Virtual Manifests):頂層專案可以不包含 [package] 區段,僅作為子專案的容器。這種結構適合發布一系列相關套件,例如:
# 頂層 Cargo.toml(虛擬清單)
[workspace]
members = ["core", "cli", "web"]
  1. 依賴分享與版本控制:工作區中的所有子專案可以分享相同版本的依賴項,這有助於避免版本不一致帶來的問題。

  2. 獨立發布:每個子專案可以獨立發布到 crates.io,使其他開發者能夠選擇性地使用你專案的某個特定部分。

許多知名的 Rust 專案都使用工作區來組織程式碼,例如 rand 套件和 Rocket 網頁框架(後者使用虛擬清單)。這種結構不僅有助於維護大型專案,還能讓使用者更靈活地選擇所需功能。

自定義構建指令碼擴充套件 Cargo 的能力

除了工作區,Cargo 還提供了另一個強大功能:自定義構建指令碼這些指令碼許我們在構建過程中執行特定操作,大擴充套件了 Rust 專案的能力邊界。

構建指令碼本質

構建指令碼質上是一個 Rust 程式,具有一個 main 函式,在專案構建時執行。雖然名為「指令碼,但它實際上會被編譯成二進位檔案執行,而非像 Shell 或 Python 指令碼樣被解釋執行。

構建指令碼過向標準輸出列印特定格式的命令來與 Cargo 通訊,Cargo 會解析這些命令並據此採取行動。

構建指令碼常見用途

在我的實踐中,發現構建指令碼別適合以下情境:

  • 編譯 C/C++ 程式碼並且 Rust 整合
  • 在編譯前處理 Rust 程式碼
  • 使用 protoc-rust 生成 Protobuf 程式碼
  • 從範本生成 Rust 程式碼
  • 執行平台檢查,如驗證特定庫存在

構建指令碼例:整合 C 程式碼

讓我們實作一個簡單的例子,使用構建指令碼一個小型 C 函式庫到 Rust 專案中。首先,建立一個新專案:

$ cargo new build-script-example
$ cd build-script-example

建立一個簡單的 C 程式碼檔案 src/hello_world.c

const char *hello_world(void) {
    return "Hello, world!";
}

更新 Cargo.toml 以包含必要的依賴:

[dependencies]
libc = "0.2"

[build-dependencies]
cc = "1.0"

這裡我們增加了兩種不同類別的依賴:

  1. libc:一個普通依賴,提供與 C 語言類別互操作的功能
  2. cc:一個構建依賴,僅在構建指令碼使用,用於編譯 C 程式碼

接下來,在專案根目錄(不是 src/ 內)建立 build.rs 檔案:

fn main() {
    println!("cargo:rerun-if-changed=src/hello_world.c");
    cc::Build::new()
        .file("src/hello_world.c")
        .compile("hello_world");
}

這個構建指令碼了兩件事:

  1. 透過 cargo:rerun-if-changed=src/hello_world.c 指令告訴 Cargo 只有當 src/hello_world.c 檔案變更時才需要重新執行此指令碼2. 使用 cc 套件編譯 C 程式碼並生成一個名為 hello_world 的靜態函式庫最後,更新 src/main.rs 以呼叫 C 函式:
use libc::c_char;
use std::ffi::CStr;

extern "C" {
    fn hello_world() -> *const c_char;
}

fn call_hello_world() -> &'static str {
    unsafe {
        CStr::from_ptr(hello_world())
            .to_str()
            .expect("String conversion failure")
    }
}

fn main() {
    println!("{}", call_hello_world());
}

這段程式碼展示瞭如何從 Rust 呼叫我們編譯的 C 函式:

  1. extern "C" 區塊宣告了外部 C 函式的介面
  2. call_hello_world() 函式包裝了對 C 函式的呼叫,處理了字串轉換
  3. 由於我們跨越了語言邊界進入 C 程式碼,必須使用 unsafe 區塊,因為 Rust 編譯器無法保證 C 程式碼的安全性

現在執行專案:

$ cargo run
Compiling cc v1.0.67
Compiling libc v0.2.91
Compiling build-script-example v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 2.26s
Running `target/debug/build-script-example`
Hello, world!

構建指令碼進階技巧

在使用構建指令碼,有幾個進階技巧值得注意:

  1. 條件編譯:可以根據目標平台或環境變數調整構建過程
// 根據目標平台調整構建
if cfg!(target_os = "linux") {
    println!("cargo:rustc-link-lib=dylib=stdc++");
} else if cfg!(target_os = "windows") {
    println!("cargo:rustc-link-lib=dylib=msvcrt");
}
  1. 設定環境變數:透過 cargo:rustc-env=NAME=VALUE 為編譯過程設定環境變數

  2. 自定義連結行為:指定連結選項、搜尋路徑等

println!("cargo:rustc-link-search=native=/path/to/libs");
println!("cargo:rustc-link-lib=static=mylib");
  1. 生成 Rust 程式碼:構建指令碼以生成 Rust 程式碼,這在處理複雜的 FFI 繫結或從其他語言生成介面時非常有用

Rust 在嵌入式環境中的應用

作為系統級程式設計語言,Rust 非常適合嵌入式程式設計,尤其是在需要明確記憶體設定和高度安全性的場景中。雖然嵌入式 Rust 本身足以成為一個獨立主題,但值得簡要探討其潛力。

Rust 在嵌入式領域的優勢

Rust 的靜態分析工具在嵌入式領域特別強大,因為在這些環境中通常更難進行執行時除錯和驗證。編譯時保證可以輕鬆驗證資源狀態、引腳選擇,並安全地執行具有分享狀態的平行操作。

根據我的觀察,Rust 在嵌入式開發中的主要優勢包括:

  1. 零成本抽象:Rust 的抽象機制不會帶來執行時開銷
  2. 記憶體安全:無需垃圾回收即可確保記憶體安全
  3. 並發安全:編譯時檢查並發問題
  4. 豐富的型別系統:可以在編譯時捕捉許多錯誤

嵌入式 Rust 的實際應用

如果你想嘗試嵌入式 Rust 開發,有幾個入門路徑:

  1. Cortex-M 裝置模擬:使用流行的 QEMU 專案可以模擬 Cortex-M 裝置,無需實際硬體即可開始學習

  2. Arduino Uno 平台ruduino 套件提供了專為 Arduino Uno 設計的可重用元件,這是一個價格實惠的入門平台

  3. RISC-V 支援:Rust 對 RISC-V 架構有初步支援,雖然硬體選項有限

Rust 編譯器根據

深入 Rust 開發工具生態系統

在 Rust 開發的旅程中,掌握適當的工具能夠顯著提升開發效率與程式碼品質。接續前面對 Cargo 的討論,我們將深入探索 Rust 生態系統中其他重要工具,幫助你在日常開發中事半功倍。

無堆積積記憶體分配解決方案

在嵌入式系統或資源受限的環境中,動態記憶體分配往往是一個挑戰。Rust 提供了優雅的解決方案 - heapless 函式庫提供固定大小與無需動態分配的資料結構。

若你確實需要動態記憶體分配功能,Rust 也提供了靈活的選擇。透過實作 GlobalAlloc 特徵,你可以輕鬆建立自己的分配器。這種方式特別適合有特殊記憶體管理需求的專案。

對於流行的 Cortex-M 處理器等嵌入式平台,已有現成的解決方案如 alloc-cortex-m 函式庫供了專為這些平台最佳化堆積積分配器實作。

Rust 核心開發工具概覽

開發 Rust 應用程式時,有幾個核心工具幾乎是每個開發者的必備武器。這些工具能夠顯著改善開發體驗,減少產生高品質軟體所需的重複工作。

Rust 開發工具的基礎

Rust 的編譯器 rustc 建立在 LLVM 之上,因此繼承了 LLVM 豐富的工具集,如 LLDB 除錯器。除了這些基本工具外,Rust 還提供了一系列特有工具,大幅提升開發效率。

以下是幾個我認為最重要的 Rust 工具:

  • rust-analyzer:提供完整的 IDE 整合支援
  • rustfmt:自動格式化程式碼
  • Clippy:提供程式碼品品檢查
  • sccache:編譯快取工具,加速大型專案的編譯

此外,有些實用工具雖然使用頻率較低,但在特定場景下極為有用,例如:cargo-updatecargo-expandcargo-fuzzcargo-watchcargo-tree

Rust 編輯器生態系統

選擇合適的編輯器能夠大幅提高開發效率。Rust 社群為各種主流編輯器提供了豐富的支援:

  • Emacs:可透過 rust-analyzer (LSP) 或 rust-mode 獲得 Rust 支援
  • IntelliJ IDEA:提供原生 Rust 支援
  • Sublime Text:可透過 LSP-rust-analyzer 或 Rust Enhanced 獲得支援
  • Vim/Neovim:可使用 rust-analyzer (LSP) 或 rust.vim
  • VS Code:rust-analyzer 提供全面支援

使用 rust-analyzer 開發完整 IDE 體驗

rust-analyzer 是目前最成熟、功能最完整的 Rust 語言編輯器工具。它實作了語言伺服器協定(LSP),可與任何支援 LSP 的編輯器整合。

安裝與設定

在 VS Code 中安裝 rust-analyzer 相當簡單,只需執行以下命令:

code --install-extension rust-lang.rust-analyzer

安裝完成後,VS Code 將為 Rust 程式碼提供豐富的功能支援,包括在 main() 函式上方顯示便捷的「Run」和「Debug」按鈕,讓你一鍵執行或除錯程式。

rust-analyzer 的強大功能

rust-analyzer 提供的功能遠超過基本的語法突顯:

  • 程式碼自動完成
  • 自動插入 import 陳述式
  • 跳轉到定義
  • 重新命名符號
  • 檔案生成
  • 程式碼重構
  • 魔法完成(Magic completions)
  • 內嵌編譯錯誤顯示
  • 型別和引數內嵌提示
  • 語義語法突顯
  • 顯示內嵌參考檔案

魔法完成功能

rust-analyzer 的字尾文字完成功能是一項能顯著提升開發效率的工具。它提供了常見任務的快速完成,如除錯列印或字串格式化。熟悉這些魔法完成能為你節省大量重複輸入的時間。

魔法完成類別於程式碼片段(snippets),但增加了一些 Rust 特有功能,使其更加強大。而與它們可以在任何支援語言伺服器協定的編輯器中使用,不僅限於 VS Code。

使用魔法完成非常簡單,只需輸入表示式並使用編輯器的完成下拉選單。例如,要在當前源檔案中建立一個測試模組,可以輸入 tmod 並選擇第一個完成結果:

// 輸入 tmod 後選擇完成選項
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_name() {
    }
}

這個完成選項會建立一個測試模組範本,包含一個可填寫的測試函式。除了 tmod 外,還有 tfn 完成選項,用於建立單個測試函式。

這段程式碼展示了 Rust 中測試模組的標準結構。#[cfg(test)] 標註表示這個模組只在執行測試時才會被編譯,這是 Rust 條件編譯的一個應用。use super::*; 匯入上層模組的所有內容,讓測試可以直接使用被測函式。#[test] 標註則標記 test_name() 為一個測試函式,會在執行 cargo test 時被自動執行。

字串格式化的魔法完成

另一個實用的魔法完成功能是字串列印。在 Rust 1.58.0 版本之前,Rust 並不支援字串插值。為了彌補這一不足,rust-analyzer 提供了多種用於列印、日誌記錄和格式化字串的完成選項。

即使在 Rust 1.58.0 版本之後加入了字串插值功能,這些魔法完成仍然是展示 rust-analyzer 強大功能的好例子,而與在許多情況下使用這些完成選項仍然比手動編寫格式化字串更加便捷。

rustfmt:維持一致的程式碼風格

在團隊協作中,保持一致的程式碼風格至關重要。rustfmt 工具提供了自動格式化 Rust 程式碼的能力,遵循官方的 Rust 風格。

使用 rustfmt 可以避免團隊成員在程式碼風格上的爭論,並確保專案風格的一致性。我在所有專案中都會設定自動格式化,這樣可以專注於解決問題,而不是擔心格式問題。

Clippy:提升程式碼品質的利器

Clippy 是 Rust 的程式碼品質工具,提供了大量程式碼品品檢查(稱為 lints)。它能夠發現常見錯誤、非慣用寫法,以及可能的效能問題。

在我的開發流程中,Clippy 是必不可少的一環。它不僅幫助我避免了許多常見錯誤,還教會了我更多 Rust 的慣用寫法。將 Clippy 整合到 CI/CD 系統中,可以確保提交的程式碼符合團隊的品質標準。

sccache:加速大型專案編譯

對於大型 Rust 專案,編譯時間可能成為一個挑戰。sccache 是一個通用的編譯器快取工具,可顯著提高大型專案的編譯速度。

它透過快取編譯結果,避免重複編譯未變更的程式碼,從而大幅減少總編譯時間。在我的工作流程中,使用 sccache 後編譯時間減少了約 40%,極大提升了開發效率。

額外實用工具

除了上述核心工具外,以下工具在特定場景下也非常有價值:

  • cargo-update:批次更新已安裝的 Cargo 工具
  • cargo-expand:展開 Rust 巨集,幫助理解和除錯
  • cargo-fuzz:模糊測試工具,自動發現潛在漏洞
  • cargo-watch:監視檔案變更並自動執行命令
  • cargo-tree:視覺化展示專案依賴關係

Rust 工具使用心得

經過多年的 Rust 開發經驗,我發現最有效的工作流程是將這些工具緊密整合到開發環境中。在 VS Code 中,我設定了儲存時自動格式化程式碼,並在背景執行 Clippy 檢查。

對於大型專案,sccache 是必備工具,它能將重複編譯的時間成本降到最低。而 rust-analyzer 的人工智慧和即時錯誤檢查功能,則讓我能夠更快地識別和修復問題。

Rust 的這套工具鏈在我看來是所有程式語言中最為完善和統一的工具生態系統之一。它們不僅提高了開發效率,還確保了程式碼的品質和一致性。無論是個人專案還是團隊協作,這些工具都能顯著改善 Rust 開發體驗。

在掌握了這些工具後,你將能夠專注於解決實際問題,而不是被繁瑣的格式調整和常見錯誤所困擾。這正是 Rust 工具鏈的價值所在 - 它讓我們能夠寫出更好的程式碼,同時享受開發的過程。 讓我們接續前面的內容,介紹Rust工具鏈的重要組成部分。

程式碼與字串格式化的魔法完成功能

let bananas = 5.0;
let apes = 2.0;
"bananas={bananas} apes={apes} bananas_per_ape={bananas / apes}"

上述程式碼示範了Rust中的字串插值語法。這段程式碼建立了兩個浮點數變數並嘗試在字串中直接參照它們。在Rust中,當你將遊標放在這段字串的結尾並輸入.print時,rust-analyzer會人工智慧其轉換為println!巨集完整呼叫格式。

當你選擇這個自動完成選項後,rust-analyzer會自動將程式碼重構為標準的Rust字串格式化語法:

let bananas = 5.0;
let apes = 2.0;
println!(
    "bananas={} apes={} bananas_per_ape={}",
    bananas,
    apes,
    bananas / apes
)

這是轉換後的程式碼,使用了Rust的標準println!巨集這種格式使用花括號{}作為佔位符,然後在字串後面依序提供要填入的引數。這種方式比起直接字串插值更符合Rust的習慣用法,與在編譯時就能檢查格式化的正確性。

常用的魔法完成功能

rust-analyzer提供了許多魔法完成功能,讓開發者能更高效地編寫Rust程式碼。以下是一些值得記住的重要完成功能:

表示式結果說明
"str {arg}".formatformat!("str {}", arg)將引數格式化為字串
"str {arg}".printlnprintln!("str {}", arg)使用引數列印字串
"".logLlog::level!("str {}", arg) 其中level可以是debug、trace、info、warn或error以指定級別記錄帶引數的字串
pdeprintln!("arg = {:?}", arg)除錯列印(輸出到stderr)
ppdeprintln!("arg = {:#?}", arg)美化除錯列印(輸出到stderr)
expr.ref&expr借用表示式
expr.refm&mut expr可變借用表示式
expr.ifif expr {}將表示式轉換為if陳述式,對於Option和Result特別有用

使用rustfmt保持程式碼整潔

程式碼格式化在多人協作的Rust專案中尤為重要。對於單人專案來說,格式化風格可能不是大問題,但一旦有多位貢獻者,程式碼風格就可能出現分歧。rustfmt是Rust為解決這一問題提供的自動化、有主見的格式化工具。

如果你來自Go語言背景,rustfmt類別於gofmt;如果你使用其他語言,也可能有類別的格式化工具。這種有主見的格式化工具是現代程式語言的一大進步。

玄貓不得不說,這類別具確實為開發者省去了無數小時的風格辯論。我無法計算在程式碼審查中花費多少時間討論格式化問題。使用rustfmt並簡單地要求程式碼貢獻遵循定義的風格,可以立即解決這個問題。與其發布和維護冗長的風格檔案,不如使用rustfmt,為所有人節省大量時間。

安裝rustfmt

rustfmt作為rustup的一個元件安裝:

$ rustup component add rustfmt
...

安裝完成後,可以透過Cargo執行:

$ cargo fmt
# Rustfmt會就地格式化你的程式碼

設定rustfmt

雖然rustfmt的預設設定對大多數人來說已經足夠,但你可能想根據自己的偏好稍微調整設定。這可以透過在專案的原始碼樹中增加.rustfmt.toml設定檔案來完成:

format_code_in_doc_comments = true
group_imports = "StdExternalCrate"
imports_granularity = "Module"
unstable_features = true
version = "Two"
wrap_comments = true

以下是rustfmt的一些設定選項:

設定預設值建議值說明
imports_granularityPreserveModule定義import陳述式的粒度
group_importsPreserveStdExternalGroup定義import分組的排序
unstable_featuresfalsetrue啟用僅限nightly的功能
wrap_commentsfalsetrue自動換行註解和程式碼
format_code_in_doc_commentsfalsetrue對檔案中的程式碼樣本應用rustfmt
versionOneTwo選擇要使用的rustfmt版本

值得注意的是,在撰寫本文時,一些重要的rustfmt選項仍是僅限nightly的功能。你可以在rustfmt官網上找到最新的可用風格選項列表。

使用Clippy提升程式碼品質

Clippy是Rust的程式碼品質工具,目前提供超過450項檢查。如果你曾經因為同事在程式碼審查中指出微小的語法、格式和其他風格改進而感到沮喪,那麼Clippy就是為你準備的。Clippy可以完成與你同事相同的工作,但不會帶有任何嘲諷,在許多情況下甚至會提供程式碼修改建議。

Clippy常能在你的程式碼中發現實際問題。然而,Clippy的真正優勢在於它消除了爭論程式碼風格問題的需要,因為它強制執行了Rust的慣用風格和模式。

安裝Clippy

Clippy作為rustup的一個元件分發,安裝如下:

$ rustup component add clippy
...

安裝完成後,你可以使用Cargo在任何Rust專案中執行Clippy:

$ cargo clippy
...

執行時,Clippy會產生類別於rustc編譯器輸出的結果。

Clippy的lint檢查

Clippy擁有超過450項程式碼品品檢查(稱為lint),可以寫一整本章來介紹。這些lint按嚴重程度(allow、warn、deny和deprecated)分類別並根據類別分組,可以是以下之一:correctness(正確性)、restriction(限制)、style(風格)、deprecated(棄用)、pedantic(吹毛求疵)、complexity(複雜度)、perf(效能)、cargo和nursery(孵化)。

一個例子是blacklisted_name lint,它禁止使用諸如foobarquux等變數名。這個lint可以設定為包含你希望停用的自定義變數名列表。

另一個例子是bool_comparison lint,它檢查表示式和布林值之間不必要的比較。例如,以下程式碼被視為無效:

if function_returning_boolean() == true {}

而以下程式碼是有效的:

if function_returning_boolean() {}

大多數Clippy的lint與風格有關,但它也可以幫助找出效能問題。例如,redundant_clone lint可以找出不必要克隆變數的情況。通常,這種情況看起來像這樣:

let my_string = String::new("my string");
println!("my_string='{}'", my_string.clone());

在上述程式碼中,對clone()的呼叫完全不必要。如果你用這段程式碼執行Clippy,你會得到警告:

$ cargo clippy
warning: redundant clone
--> src/main.rs:3:37
|
3 | println!("my_string='{}'", my_string.clone());
| ^^^^^^^^ help: remove this
|
= note: `#[warn(clippy::redundant_clone)]` on by default
note: this value is dropped without further use
--> src/main.rs:3:28
|
3 | println!("my_string='{}'", my_string.clone());
| ^^^^^^^^^
= help: for further information visit
https://rust-lang.github.io/rust-clippy/master/index.html#redundant_clone
warning: 1 warning emitted

Clippy會定期更新,你可以在官方網站找到穩定版Rust的最新lint列表。