Rust 函式庫的設計需要兼顧功能性、易用性和可維護性。良好的命名慣例、清晰的程式碼結構和完善的檔案是提升函式庫美學的關鍵。本文以 LinkedList 的開發過程為例,示範如何運用 Rust 的特性,如檔案測試、迭代器和 Debug 特徵,開發高品質的函式庫。從程式碼範例到整合測試,文章逐步引導讀者理解 Rust 函式庫設計的最佳實務,並強調了 API 設計一致性、檔案撰寫的完整性和測試的重要性,以確保函式庫的健壯性和易用性。

設計一個優質的函式庫:從美學到實作

在軟體開發的世界中,函式庫(library)的設計不僅僅是技術實作的展現,更是使用者經驗的體現。一個優秀的函式庫不僅需要具備強大的功能性,更需要在設計上考慮到易用性、可讀性和可維護性。本文將探討設計一個優質函式庫的關鍵要素,從美學到具體實作,為讀者提供全面的指導。

考慮美學設計

第一印象至關重要,而函式庫的美學設計對使用者對其的整體感受有著巨大的影響。美學不僅僅是指函式庫的外觀,更包括了使用者的體驗。一個易於使用、理解和除錯的函式庫,會被認為是更具美學價值的。

函式庫的美學受到多種因素的影響,包括型別、函式和變數的命名,程式碼的結構,檔案說明,範例程式碼,以及整體設計。一個組織良好、檔案齊全、易於使用的函式庫,比起那些雜亂無章、檔案缺乏、難以使用的函式庫,更具美學吸引力。

在設計函式庫時,需要考慮程式碼、檔案和範例的美學。使用一致的命名慣例,邏輯地組織程式碼,並提供清晰、簡潔、語法正確、無錯誤的檔案說明。好的檔案工具可以使這項任務變得更加容易。撰寫範例時,應簡單直接地展示如何使用函式庫。考慮使用者的體驗,努力使其盡可能愉快。

程式碼範例:命名慣例的重要性

// 不良命名範例
fn calc(a: i32, b: i32) -> i32 {
    a + b
}

// 良好命名範例
/// 計算兩個整數的總和
fn calculate_sum(a: i32, b: i32) -> i32 {
    a + b
}

內容解密:

在上述範例中,我們展示了命名慣例的重要性。良好的命名可以讓程式碼更易於理解和維護。在 calculate_sum 函式中,我們使用了清晰的命名來描述函式的功能,這使得其他開發者能夠快速理解其用途。

探索 Rust 函式庫的人體工程學

讓我們透過建立一個根據之前程式碼範例的函式庫來綜合運用目前為止所學到的知識。這是一個非常適合練習編寫函式庫的練習。透過從最終使用者的角度來編寫檔案和測試程式碼,可以學到很多。此外,我認為迫使自己從其他使用者的角度關注細節,可以幫助你編寫出更好的程式碼。編寫函式庫可以強制你進行封裝、分離關注點和建立良好的介面。

建立鏈結串列函式庫

首先,我們將使用 cargo new --lib linkedlist 建立一個新的函式庫專案。然後,將第三章中的鏈結串列程式碼複製到 src/lib.rs 中。接下來,在 tests/integration_test.rs 中建立一個整合測試,並將之前的測試程式碼複製到其中。

#[test]
fn test_linkedlist() {
    use linkedlist::LinkedList;

    let mut linked_list = LinkedList::new("first item");
    // ... 省略其他程式碼 ...
}

內容解密:

在這個範例中,我們展示瞭如何建立一個整合測試來驗證我們的鏈結串列函式庫。透過使用 cargo new --lib linkedlist 命令,我們建立了一個新的 Rust 函式庫專案。然後,我們將鏈結串列的實作複製到 src/lib.rs 中,並在 tests/integration_test.rs 中撰寫了測試程式碼,以驗證函式庫的功能。

使用 rustdoc 改善 API 設計

接下來,我們將檢查函式庫的 API。最好的方法是使用 rustdoc 產生檔案。任何使用我們函式庫的人都可能會花很多時間檢視我們的檔案,因此,如果我們希望別人能夠成功地使用我們的函式庫,那麼擁有高品質的檔案是非常重要的。

執行 cargo doc 命令可以產生 HTML 檔案,並將其放置在 crate 的 target/doc 目錄中。開啟 linkedlist/index.html 檔案,這是我們 crate 檔案的首頁。

檔案註解範例

//! # 鏈結串列(linkedlist)crate
//!
//! 本 crate 提供了一個簡單的鏈結串列實作。
//!
//! 本 crate 作為 [_Rust Advanced Techniques_](https://www.manning.com/books/idiomatic-rust) 一書的教學範例。

內容解密:

在上述範例中,我們展示瞭如何使用 rustdoc 的註解來為我們的 crate 和其功能提供說明。這樣的註解不僅能夠幫助其他開發者瞭解如何使用我們的函式庫,也能夠在產生的檔案中提供清晰的指引。

設計函式庫:Rust 連結串列實作與檔案撰寫

在開發 Rust 函式庫時,良好的檔案撰寫和 API 設計是至關重要的。本章節將探討如何為我們的 LinkedList 實作撰寫適當的檔案,並檢視其 API 設計的合理性。

檔案撰寫與測試

當我們為 LinkedList 新增檔案時,可以使用 Rust 的內建檔案工具 rustdoc。以下是一個範例:

//! ## 使用範例
//!
//! ```rust
//! use linkedlist::LinkedList;
//!
//! let mut animals = LinkedList::new();
//! animals.append("chicken");
//! animals.append("ostrich");
//! animals.append("antelope");
//! animals.append("axolotl");
//! animals.append("okapi");
//! ```

重新生成檔案後,我們的函式庫層級檔案看起來如圖 6.3 所示。為了使檔案保持最新,可以使用 cargo watch -x doc 命令自動重新生成檔案。

檔案測試

Rust 的檔案範例會自動成為整合測試的一部分。當執行 cargo test 時,我們可以看到檔案範例被執行:

$ cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs
(target/debug/deps/linkedlist-2e0286b0918288ae)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;
finished in 0.00s
Running tests/integration_test.rs
(target/debug/deps/integration_test-c95f81c9911957c8)
running 1 test
test test_linkedlist ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;
finished in 0.00s
Doc-tests linkedlist
running 1 test
test src/lib.rs - (line 10) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;
finished in 0.26s

值得注意的是,rustdoc 會自動為檔案範例新增 fn main() { ... } 包裝,使其成為可執行的測試程式碼。

API 設計與改進

在撰寫 LinkedList 的過程中,我們並未過多考慮其 API 的設計。現在檢視我們的實作,可以發現 new() 方法的設計與其他集合型別(如 Vec)不一致。為了保持一致性,我們應該更新 new() 方法,使其傳回一個空的 LinkedList

更新 LinkedList 實作

以下是更新後的 LinkedList 實作:

/// 提供單向鏈結串列實作,支援迭代器存取。
pub struct LinkedList<T> {
    head: Option<ListItemPtr<T>>,
}

impl<T> LinkedList<T> {
    /// 建構一個新的空 [`LinkedList<T>`]。
    pub fn new() -> Self {
        Self { head: None }
    }

    /// 將元素附加到串列末尾。如果串列為空,則該元素成為串列的第一個元素。
    pub fn append(&mut self, t: T) {
        match &self.head {
            Some(head) => {
                let mut next = head.clone();
                while next.as_ref().borrow().next.is_some() {
                    let n = next.as_ref().borrow().next.as_ref().unwrap().clone();
                    next = n;
                }
                next.as_ref().borrow_mut().next = Some(Rc::new(RefCell::new(ListItem::new(t))));
            }
            None => {
                self.head = Some(Rc::new(RefCell::new(ListItem::new(t))));
            }
        }
    }

    // ... 其他方法實作 ...
}

檔案與實作的改進

更新後的 LinkedList 檔案如圖 6.4 所示。雖然我們的實作已經具備基本功能,但仍有兩項重要的功能缺失:列印串列內容和複製串列。

複製(Clone)實作

為了實作 Clone 特徵,我們需要考慮複製鏈結串列的含義。最合理的實作是複製串列的結構和內容,而不僅僅是複製指標。

以下是我們的 Clone 實作:

impl<T: Clone> Clone for LinkedList<T> {
    fn clone(&self) -> Self {
        let mut cloned = Self::new();
        cloned.clone_from(self);
        cloned
    }

    fn clone_from(&mut self, source: &Self) {
        self.head = None;
        source.iter().for_each(|item| {
            self.append(item.clone())
        });
    }
}

這個實作首先建立一個新的空串列,然後透過迭代原始串列,將每個元素複製到新的串列中。

未來,我們可以考慮為 LinkedList 新增更多功能,例如支援更多的迭代器方法、最佳化效能等。同時,也可以進一步改進檔案的撰寫,使其更為詳細和易於理解。

LinkedList 結構示意圖

  graph LR;
    A[Head] -->|Option<ListItemPtr<T>>|> B[ListItem<T>];
    B -->|next|> C[ListItem<T>];
    C -->|next|> D[...];
    D -->|next|> E[None];

圖表翻譯: 此圖示展示了 LinkedList 的基本結構。Head 是指向第一個 ListItem 的可選指標,每個 ListItem 都包含一個指向下一個專案的 next 指標,直到最後一個專案的 nextNone

程式碼解析

以下是一個簡單的測試程式,用於驗證我們的 LinkedList 實作:

fn main() {
    let mut list = LinkedList::new();
    list.append(1);
    list.append(2);
    list.append(3);

    for item in list.iter() {
        println!("{}", item);
    }
}

內容解密:

  1. 首先,我們建立一個新的空 LinkedList
  2. 然後,我們使用 append 方法將三個元素新增到串列中。
  3. 最後,我們使用 iter 方法迭代串列,並列印每個元素的值。

這個範例展示瞭如何使用我們的 LinkedList 實作,以及如何迭代其元素。

6.11 檢視 Rust 函式庫的易用性

在前面的章節中,我們成功地簡化了程式碼,並遵循了 DRY(Don’t Repeat Yourself)原則。這意味著對 clone_from() 的任何修改都會反映在 clone() 中。

6.11.3 透過更多測試來改進我們的鏈結串列

由於我們新增了許多新功能,因此應該對程式碼進行測試。讓我們更新整合測試,以分別測試每個功能。首先,我們將測試 LinkedListiter() 方法,如下所示:

#[test]
fn test_linkedlist_iter() {
    use linkedlist::LinkedList;
    let test_data = vec!["first", "second", "third", "fourth", "fifth and last"];
    let mut linked_list = LinkedList::new();
    test_data.iter().for_each(|s| linked_list.append(s.to_string()));
    assert_eq!(
        test_data,
        linked_list.iter().map(|s| s.as_str()).collect::<Vec<&str>>()
    );
}

內容解密:

此測試函式首先建立一個包含測試資料的向量 test_data,然後建立一個新的 LinkedList。接著,將測試資料逐一加入到鏈結串列中。最後,透過 iter() 方法遍歷鏈結串列,並將結果與原始測試資料進行比較,以確保兩者一致。

接下來,我們測試可變迭代器方法 iter_mut()

#[test]
fn test_linkedlist_iter_mut() {
    use linkedlist::LinkedList;
    let test_data = vec!["first", "second", "third", "fourth", "fifth and last"];
    let mut linked_list = LinkedList::new();
    test_data.iter().for_each(|s| linked_list.append(s.to_string()));
    assert_eq!(
        test_data,
        linked_list.iter_mut().map(|s| s.as_str()).collect::<Vec<&str>>()
    );
}

內容解密:

此測試與前一個類別似,但使用 iter_mut() 方法來遍歷鏈結串列。這確保了可變迭代器的正確性。

我們還需要測試 into_iter() 方法:

#[test]
fn test_linkedlist_into_iter() {
    use linkedlist::LinkedList;
    let test_data = vec!["first", "second", "third", "fourth", "fifth and last"];
    let mut linked_list = LinkedList::new();
    test_data.iter().for_each(|s| linked_list.append(s.to_string()));
    assert_eq!(
        test_data.iter().map(|s| s.to_string()).collect::<Vec<String>>(),
        linked_list.into_iter().collect::<Vec<String>>()
    );
}

內容解密:

此測試驗證了 into_iter() 方法的正確性。透過比較轉換後的測試資料與 into_iter() 傳回的迭代器結果,確保兩者的一致性。

最後,我們測試 Clone 特徵的實作:

#[test]
fn test_linkedlist_cloned() {
    use linkedlist::LinkedList;
    let test_data = vec!["first", "second", "third", "fourth", "fifth and last"];
    let mut linked_list = LinkedList::new();
    test_data.iter().for_each(|s| linked_list.append(s.to_string()));
    let cloned_list = linked_list.clone();
    linked_list.into_iter().zip(cloned_list.into_iter()).for_each(|(left, right)| {
        assert_eq!(left, right);
        assert!(!std::ptr::eq(&left, &right));
    });
}

內容解密:

此測試驗證了複製的鏈結串列與原始串列的一致性,並確保複製的資料位於不同的記憶體位置。

6.11.4 使我們的函式庫更容易除錯

接下來,讓我們討論 Debug 特徵。預設情況下,使用 #[derive(Debug)] 並透過 dbg!(linked_list) 列印我們的鏈結串列可能會產生難以閱讀的輸出。為瞭解決這個問題,我們需要手動實作 Debug 特徵。

Rust 提供了 Formatter 工具來處理輸出的格式化。我們可以使用 Formatter::debug_list() 來簡化實作:

impl<T: Debug> Debug for LinkedList<T> {
    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        fmt.debug_list().entries(self.iter()).finish()
    }
}

內容解密:

此實作使用 debug_list() 方法來格式化鏈結串列的輸出,使其更易於閱讀。透過遍歷鏈結串列並將元素加入到格式化輸出中,產生更友善的除錯資訊。

實作後,我們的輸出將變得更加清晰:

[tests/integration_test.rs:20] linked_list = [
    "first",
    "second",
    "third",
    "fourth",
    "fifth and last",
]

最後,我們為迭代器撰寫檔案,以補充說明。雖然 Iterator 特徵已經提供了許多函式的檔案,但我們仍需為自定義的迭代器提供簡要說明。