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 產生文件,能讓迭代速度提升非常多。
文件的產生流程大致是這樣的:
flowchart LR
A["原始碼 + 文件註解"] --> B["rustdoc 解析模組結構"]
B --> C["抽取 /// 與 //! 註解"]
C --> D["Markdown 轉換為 HTML"]
D --> E["插入型別簽章與交叉連結"]
E --> F["產出 target/doc/index.html"]
F --> G["cargo publish 上傳到 crates.io"]
G --> H["docs.rs 自動建置並公開"]最後兩步是 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 章開始的實戰專案所要建立的基礎。