本文 AI 產出,尚未審核

Rust 泛型與特徵 – 預設方法

簡介

在 Rust 中,**特徵(trait)**是抽象行為的核心。它不僅讓我們可以在不同型別之間共享介面,還能透過 預設方法(default methods) 為特徵提供「即插即用」的實作。這樣一來,實作者只需要關注少數關鍵方法,其餘行為即可自動繼承,極大降低重複程式碼的風險。

對於 初學者,預設方法是理解特徵與抽象的第一步;對於 中階開發者,則是打造彈性 API、擴充第三方型別的利器。掌握預設方法後,你可以:

  • 為特徵提供完整的行為描述,同時允許使用者自行覆寫。
  • 在不破壞相容性的前提下,為已發佈的特徵加入新功能。
  • 以最小的實作成本,讓自訂型別立即具備豐富的功能。

下面我們將一步步拆解預設方法的概念、使用方式與實務技巧。


核心概念

1. 什麼是預設方法?

在 Rust 的特徵定義中,方法可以只宣告簽名(抽象),也可以直接提供 預設實作。語法上,只要在特徵內寫出完整的函式本體,即可成為預設方法。

trait Greet {
    // 必須實作的方法
    fn name(&self) -> &str;

    // 預設方法:自動使用 `name` 產生問候語
    fn hello(&self) -> String {
        format!("Hello, {}!", self.name())
    }
}
  • name 必須由實作者自行提供。
  • hello 只要 name 可用,就不需要額外實作。

重點:預設方法本身可以呼叫其他抽象方法,形成「模板方法模式」。

2. 為何要使用預設方法?

場景 沒有預設方法 有預設方法
API 穩定性 每次新增功能都必須改動所有實作 只需在特徵內新增預設實作,舊有程式碼不受影響
減少重複 多個型別各自寫相同的程式碼 只寫一次,所有型別自動共享
擴充性 只能在原始型別內部提供行為 任何實作此特徵的型別皆可受惠

3. 預設方法的限制

  • 不能使用 Self 的未實作方法:預設方法只能呼叫已在特徵中聲明(抽象或已提供預設)的函式,否則編譯會錯誤。
  • 不支援泛型參數的預設實作(在特徵本身的泛型參數上有限制),若需要更複雜的行為,往往需要使用 關聯型別(associated types)where 子句

4. 覆寫預設方法

實作者可以選擇保留預設實作,或自行覆寫以提供更有效率或更符合需求的版本。

struct Person {
    first: String,
    last: String,
}

impl Greet for Person {
    fn name(&self) -> &str {
        // 這裡僅示範,實際上需要返回 &str,簡化為 &self.first
        &self.first
    }

    // 覆寫預設的 hello,加入姓氏資訊
    fn hello(&self) -> String {
        format!("Hello, {} {}!", self.first, self.last)
    }
}

5. 多個預設方法的相互依賴

預設方法可以相互呼叫,形成「層層堆疊」的行為。以下示範一個 計算型別大小 的特徵:

trait Size {
    fn width(&self) -> usize;
    fn height(&self) -> usize;

    // 預設方法:計算面積
    fn area(&self) -> usize {
        self.width() * self.height()
    }

    // 預設方法:判斷是否為正方形
    fn is_square(&self) -> bool {
        self.width() == self.height()
    }
}

只要實作 widthheightareais_square 立即可用,且不需要額外程式碼。


程式碼範例

以下提供 5 個實用範例,涵蓋從基礎到進階的預設方法應用。

範例 1:基本預設方法(問候語)

trait Greet {
    fn name(&self) -> &str;

    fn hello(&self) -> String {
        format!("Hello, {}!", self.name())
    }
}

struct Dog {
    name: String,
}

impl Greet for Dog {
    fn name(&self) -> &str {
        &self.name
    }
}

fn main() {
    let d = Dog { name: "Buddy".into() };
    println!("{}", d.hello()); // Hello, Buddy!
}

說明Dog 只實作 namehello 直接使用預設實作。

範例 2:覆寫預設方法(自訂問候)

impl Greet for Dog {
    fn name(&self) -> &str {
        &self.name
    }

    fn hello(&self) -> String {
        format!("汪汪!我是 {},很高興見到你!", self.name())
    }
}

說明:覆寫 hello 後,行為改為中文且加入「汪汪」音效。

範例 3:預設方法使用關聯型別

trait IteratorExt {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // 預設方法:收集成 Vec
    fn collect_vec(&mut self) -> Vec<Self::Item>
    where
        Self: Sized,
    {
        let mut vec = Vec::new();
        while let Some(item) = self.next() {
            vec.push(item);
        }
        vec
    }
}

說明:只要實作 nextcollect_vec 立即可用,類似標準庫的 Iterator::collect

範例 4:多層預設方法(幾何圖形)

trait Shape {
    fn perimeter(&self) -> f64;

    // 預設方法:計算對角線(僅對矩形有效)
    fn diagonal(&self) -> f64 {
        // 預設回傳 0,讓不支援的型別自行覆寫
        0.0
    }
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Shape for Rectangle {
    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }

    fn diagonal(&self) -> f64 {
        (self.width.powi(2) + self.height.powi(2)).sqrt()
    }
}

說明Shape 提供 perimeter 必須實作,diagonal 預設回傳 0.0,讓不支援此功能的型別仍能編譯。

範例 5:預設方法與泛型限制(排序)

trait Sortable {
    fn sort(&mut self);

    // 預設方法:檢查是否已排序
    fn is_sorted(&self) -> bool
    where
        Self: PartialOrd,
    {
        // 這裡使用簡易檢查,實際可更有效率
        let mut iter = self.iter();
        let mut prev = match iter.next() {
            Some(v) => v,
            None => return true,
        };
        for cur in iter {
            if prev > cur {
                return false;
            }
            prev = cur;
        }
        true
    }

    // 需要實作的輔助方法
    fn iter(&self) -> std::slice::Iter<'_, i32>;
}

impl Sortable for Vec<i32> {
    fn sort(&mut self) {
        self.sort_unstable();
    }

    fn iter(&self) -> std::slice::Iter<'_, i32> {
        self.iter()
    }
}

說明SortableVec<i32> 提供 sort 必須實作,is_sorted 為預設方法,利用 PartialOrd 約束進行比較。


常見陷阱與最佳實踐

陷阱 描述 解決方式
忘記在特徵中宣告抽象方法 預設方法呼叫了未宣告的抽象方法,編譯錯誤。 確保所有預設方法使用的函式在特徵內都有簽名。
預設方法過於龐大 大量計算或 I/O 會影響所有實作的效能。 把重度運算抽成獨立函式,讓實作者自行決定是否呼叫。
過度依賴關聯型別 使特徵變得難以實作,特別是對外部型別。 僅在必要時使用 type Item,否則使用泛型參數。
忘記加上 where Self: Sized 某些預設方法需要 Self 為具體型別(例如回傳 Self),否則編譯失敗。 在需要的預設方法前加上 where Self: Sized
預設實作與未來版本衝突 新增預設方法後,舊有實作若不符合新行為,可能產生語意錯誤。 使用 語意版本化(semver),在大幅變更時提供 #[deprecated] 或新特徵。

最佳實踐

  1. 保持預設方法簡潔:只做「輔助」或「組合」工作,避免在預設方法內寫大量邏輯。
  2. 使用文件說明:在特徵的文檔中明確指出哪些方法是「必須」實作、哪些是「可自行覆寫」。
  3. 提供測試範例:在庫的 tests/ 目錄放置特徵的預設行為測試,確保未來改動不會破壞相容性。
  4. 考慮未來擴充:若預計日後會加入新功能,先以預設方法的形式加入,讓使用者自行決定是否覆寫。

實際應用場景

  1. 日誌框架
    trait Logger { fn log(&self, msg: &str); fn info(&self, msg: &str) { self.log(&format!("[INFO] {}", msg)) } }
    使用者只需實作 loginfowarnerror 等級皆自動提供。

  2. 資料庫抽象層
    trait Repository<T> { fn insert(&self, item: T); fn find(&self, id: i64) -> Option<T>; fn delete(&self, id: i64) { /* 預設使用 find + remove */ } }
    只要實作 insertfind,刪除操作即可直接使用預設實作。

  3. 序列化/反序列化
    標準庫的 serde::SerializeDeserialize 皆利用預設方法提供 to_stringfrom_str 等便利函式,讓自訂型別只要實作核心的 serialize/deserialize 即可。

  4. 圖形 UI 元件
    trait Widget { fn draw(&self); fn resize(&mut self, w: u32, h: u32) { /* 預設保持比例 */ } }
    大多數元件只需要實作 draw,而 resize 依需求自行覆寫。

  5. 測試框架
    trait TestCase { fn run(&self); fn name(&self) -> &str; fn description(&self) -> &str { "" } }
    測試套件只要提供 runname,說明文字可使用預設空字串。


總結

  • 預設方法 是 Rust 特徵中提升抽象層次、減少重複程式碼的關鍵工具。
  • 它允許 模板方法 的寫法,使得只要實作少數核心抽象,就能自動獲得完整的行為集合。
  • 使用時要注意 抽象方法的宣告效能影響、以及 相容性,並遵循 簡潔、文件化、測試化 的最佳實踐。
  • 在日誌、資料庫、序列化、UI、測試等多種實務領域,預設方法都能大幅提升開發效率與程式碼可維護性。

掌握了預設方法,你就能在 Rust 生態中寫出更具彈性、可擴充且易於維護的程式碼。祝你在 Rust 的旅程中玩得開心,寫出更好、更安全的程式!