跳轉到

15.3 自動生成 API 文件(cargo doc)#

從註解到 HTML#

Rust 的 API 文件由 rustdoc 產生,而 rustdoc 是標準工具鏈的一部分。它讀取你的原始碼中的文件註解(doc comment),結合型別資訊與原始碼結構,產生一份帶有搜尋功能、可點擊跳轉的 HTML 文件。

cargo doc              # 產生文件到 target/doc
cargo doc --open       # 產生後自動開啟瀏覽器
cargo doc --no-deps    # 只產生自己的 crate,不包含相依套件

--no-deps 參數相當實用。若一個專案相依了 serde、tokio 這類大型套件,預設會連帶把這些相依的文件也建置出來,耗費大量時間與磁碟空間。只為自己的 crate 產生文件,能讓迭代速度提升非常多。

文件的產生流程大致是這樣的:

最後兩步是 Rust 生態系很重要的一項基礎設施。只要套件被發布到 crates.io,docs.rs 就會自動為它建置文件並在網路上提供瀏覽,使用者不需要在本機安裝任何東西就能閱讀你的文件。

三種文件註解#

Rust 提供三種文件註解的寫法,各自適用於不同的位置。

行內文件(///)用於項目之前,例如函式、結構體、欄位、列舉變體。

/// 將兩個數值相加。
///
/// # 範例
///
/// ```
/// assert_eq!(add(2, 2), 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

模組層級文件(//!)寫在模組內的最開頭,用來描述整個模組的用途。它必須出現在任何項目之前。

//! # 堆疊實作
//!
//! 這個模組提供一個以 `Vec` 為基礎的 LIFO 堆疊。
//!
//! ```
//! use stacker::Stack;
//!
//! let mut s = Stack::new();
//! s.push(1);
//! assert_eq!(s.pop(), Some(1));
//! ```

pub struct Stack {
    items: Vec<i32>,
}

區塊文件(/** */)是較少使用的形式,適合需要寫很長說明的場合。它與 /// 同樣是行內文件,只是以 C 風格的區塊包裹。

/**
 * 計算集合的平均值。
 *
 * 若集合為空則回傳 `None`。
 */
pub fn mean(values: &[f64]) -> Option<f64> {
    if values.is_empty() {
        return None;
    }
    Some(values.iter().sum::<f64>() / values.len() as f64)
}

在文件撰寫上,/// 是主流選擇。除了慣用之外,它在處理巢狀程式碼區塊時不易出錯,因為不需要注意區塊註解的結尾位置。

Markdown 支援與常用標記#

rustdoc 完整支援 Markdown,並額外提供了一些方便撰寫 API 文件的擴充。段落、程式碼區塊、列表、表格、連結都是標準語法。

警告框可以透過引用區塊的語法達成,讓重要的說明在文件中更醒目:

/// # 警告
///
/// > 這個函式會直接覆寫目標檔案,請在呼叫前確認路徑正確。
pub fn overwrite(path: &str) -> std::io::Result<()> {
    std::fs::write(path, b"")
}

文件中的程式碼區塊會被 rustdoc 擷取並轉為文件測試。這是第 15.1 節提到的機制在文件生成流程中的延伸:範例程式碼不只是展示用法,它真的會被編譯與執行,因此在文件建置的同時就驗證了 API 的可用性。

連結可以用 [型別名] 的語法指向 crate 內的項目,rustdoc 會自動轉換成可點擊的連結。指向外部的連結則使用完整的 URL。

程式碼區塊的標註方式#

文件測試的行為可以透過程式碼區塊的屬性來控制,這在需要展示不能用於獨立執行的程式碼時很重要。

標註 行為
```rust 編譯並執行,用於可直接執行的範例
```rust,no_run 只編譯不執行,用於需要網路或檔案系統的範例
```rust,ignore 完全不編譯,用於概念性片段
```text 純文字區塊,不視為程式碼
```compile_fail 必須編譯失敗,用於展示錯誤用法

no_run 與 ignore 的差別在於 no_run 仍會被型別檢查,因此能確保範例中的 API 用法是正確的;ignore 則完全跳過檢查,寫錯了也不會被發現。能用 no_run 就不要用 ignore,因為前者仍保有驗證價值。

/// # 範例
///
/// 這個範例需要網路連線,因此只編譯不執行。
///
/// ```rust,no_run
/// let resp = reqwest::blocking::get("https://example.com")?.text()?;
/// println!("{resp}");
/// # Ok::<(), reqwest::Error>(())
/// ```
pub fn fetch(url: &str) -> Option<String> {
    None
}

隱藏內部項目#

有些項目是實作細節,不該出現在公開 API 文件中。#[doc(hidden)] 可以把項目從文件裡隱藏,同時不影響它仍然是公開可用的。

/// 內部使用的快速路徑,不屬於公開 API。
#[doc(hidden)]
pub fn __fast_path_v2(buf: &mut [u8]) -> usize {
    buf.len()
}

#[doc(hidden)] 與 pub(crate) 是兩種不同的隱藏。前者只是不產生文件,外部使用者仍然可以呼叫;後者則從語意上就限制在 crate 內部。如果目的是「不讓外部依賴」,應該用 pub(crate);如果目的是「文件版面太亂」,才用 #[doc(hidden)]。

文件中的數學與公式#

這裡需要說明一個與其他領域文件慣例的差異。Rust 生態系的 API 文件幾乎不使用 TeX 風格的行內數學或顯示公式,而是採用純文字或 ASCII 圖形描述複雜度與數學關係。

常見的寫法是直接用文字標明複雜度,把複雜度當成一個小節的標題:

/// # 複雜度
///
/// 時間複雜度與空間複雜度皆為 O(n)。

或者在需要視覺化時使用 ASCII 圖,例如描述某些資料結構的查找路徑,或在文件測試的說明中畫出狀態機的轉移。

這個慣例背後有實際理由。Rust 文件的渲染環境是瀏覽器而非排版引擎,若要支援 LaTeX 就必須引入 MathJax 之類的外部相依,而 crates.io 上的文件允許使用自訂的資源標記,把這類相依推給了使用者的網路環境。純文字的描述雖然不華麗,但在任何地方都能正確顯示,對一份要給全球開發者閱讀的文件來說,這是更穩妥的選擇。

相對地,在紙本或電子書形式的教材中,行內數學與顯示公式能承載更精確的表達。兩種風格各有其合理性,判斷的依據是文件的載體:瀏覽器中的 API 文件以可攜與可搜尋為先,書籍版面則以表達的精確性為先。

直接使用 rustdoc#

cargo doc 是最方便的入口,因為它會自動處理相依性、路徑與建置選項。但有時你需要直接呼叫 rustdoc:

rustdoc --edition 2021 src/lib.rs -o target/doc
rustdoc --test src/lib.rs    # 只執行文件測試

rustdoc --test 只跑文件測試,不建置 HTML,這在只想快速確認範例是否正確時很好用。cargo test --doc 則是 Cargo 層級的等價指令,且會處理相依的編譯。

發布到 docs.rs#

發布流程本身很簡單。撰寫好文件與 Cargo.toml 後,執行:

cargo doc --no-deps    # 先本地確認
cargo publish          # 上傳到 crates.io

cargo 會先在本地建置一次 crate 以確保它能通過驗證,然後打包上傳。crates.io 收到後,docs.rs 的機器人會偵測到新版本,自動 checkout 該版本、執行 cargo doc 並把結果公開發布。整個過程不需要你在伺服器上做任何設定。

實務上的建議是發布前先跑一次 cargo doc --no-deps 並點開瀏覽結果。文件註解裡的 Markdown 語法錯誤、錯誤的連結、拼錯的項目名稱,在終端機輸出裡看不出來,只有在瀏覽器裡才會發現。

撰寫品質的建議#

範例優先是最有效的原則。對於一個不熟悉的 API 而言,看一段可以直接執行的程式碼,比讀三段抽象描述更能理解用法。範例應該展示真實的典型情境,而不是玩具級的最小片段。

其次是解釋「為什麼」而非「是什麼」。型別簽章已經說明了「是什麼」,文件的價值在於「為什麼這樣設計」「什麼時候該用」「有哪些陷阱」。sort 是什麼,看簽章就懂;但它是否穩定、是否配置記憶體、複雜度是多少,才是使用者需要的資訊。

第三是錯誤條件與 panic 說明。凡是會 panic 的函式,都應該在文件中說明在什麼條件下會 panic,以及呼叫者應該如何避免。

最後是# Safety 段落。對於 unsafe 函式,Rust 要求文件必須包含 # Safety 段落,說明呼叫者必須承擔哪些不變量(invariant)。這個段落不是選填的建議,而是安全契約的正式敘述。

/// # Safety
///
/// 呼叫者必須保證 `ptr` 指向至少 `len` 個初始化的 `u8`,
/// 且這段記憶體在呼叫期間不會被其他執行緒存取。
///
/// # Panics
///
/// 若 `len` 為零且 `ptr` 為空指標,函式會 panic。
///
/// # Examples
///
/// ```
/// # use stacker::read_first;
/// let data = [1u8, 2, 3];
/// let first = unsafe { read_first(data.as_ptr(), data.len()) };
/// assert_eq!(first, Some(1));
/// ```
pub unsafe fn read_first(ptr: *const u8, len: usize) -> Option<u8> {
    if len == 0 {
        panic!("len must be positive");
    }
    if ptr.is_null() {
        return None;
    }
    Some(*ptr)
}

# Panics 段落可以取代 panic 的發生條件說明,讓文件資訊更完整。

一個完整的文件註解範例#

底下把模組文件、函式文件、錯誤條件與範例程式碼放在一起,展示一份完整的文件風格。

//! # Stack
//!
//! 一個以 `Vec` 為基礎的後進先出堆疊。
//!
//! 這個型別刻意保持簡單:它不提供迭代器,也不處理 `Sync`。
//! 若你需要這些能力,`Vec` 本身或 [`std::collections::VecDeque`]
//! 通常是更好的選擇。
//!
//! ## 範例
//!
//! ```
//! use stacker::Stack;
//!
//! let mut s = Stack::new();
//! s.push(1);
//! s.push(2);
//! assert_eq!(s.pop(), Some(2));
//! assert_eq!(s.len(), 1);
//! ```

/// 一個後進先出的堆疊。
#[derive(Debug, Default)]
pub struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    /// 建立一個空的堆疊。
    ///
    /// # Examples
    ///
    /// ```
    /// use stacker::Stack;
    ///
    /// let s: Stack<i32> = Stack::new();
    /// assert!(s.is_empty());
    /// ```
    pub fn new() -> Self {
        Stack { items: Vec::new() }
    }

    /// 將一個值放上堆疊頂端。
    ///
    /// # Examples
    ///
    /// ```
    /// use stacker::Stack;
    ///
    /// let mut s = Stack::new();
    /// s.push("hello");
    /// ```
    pub fn push(&mut self, value: T) {
        self.items.push(value);
    }

    /// 取出堆疊頂端的值,若堆疊為空則回傳 `None`。
    ///
    /// # Examples
    ///
    /// ```
    /// use stacker::Stack;
    ///
    /// let mut s = Stack::new();
    /// assert_eq!(s.pop(), None);
    /// s.push(1);
    /// assert_eq!(s.pop(), Some(1));
    /// ```
    pub fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }

    /// 回傳堆疊中元素的數量。
    pub fn len(&self) -> usize {
        self.items.len()
    }

    /// 若堆疊中沒有任何元素則回傳 `true`。
    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }
}

這份文件值得留意的地方有幾個。模組層級的文件明確說明了這個型別不做什麼,這對讀者判断是否合適極有幫助。每個公開函式都有可執行的範例,讀者可以把它貼進自己的專案直接驗證。文件裡的 [std::collections::VecDeque] 會被 rustdoc 轉成跨 crate 的有效連結。整份文件的句子簡短,沒有重複簽章已經說明的內容。

寫好文件之後,第 15.2 節的 Clippy 會檢查程式碼本身,第 15.1 節的測試會驗證行為正確,而 rustdoc 的文件測試會確保範例程式碼不會過期。三道關卡互相配合,才能讓一個 crate 真正達到可以被他人安心使用的程度——這也是第 16 章開始的實戰專案所要建立的基礎。