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()
}
}
只要實作 width 與 height,area 與 is_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只實作name,hello直接使用預設實作。
範例 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
}
}
說明:只要實作
next,collect_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()
}
}
說明:
Sortable為Vec<i32>提供sort必須實作,is_sorted為預設方法,利用PartialOrd約束進行比較。
常見陷阱與最佳實踐
| 陷阱 | 描述 | 解決方式 |
|---|---|---|
| 忘記在特徵中宣告抽象方法 | 預設方法呼叫了未宣告的抽象方法,編譯錯誤。 | 確保所有預設方法使用的函式在特徵內都有簽名。 |
| 預設方法過於龐大 | 大量計算或 I/O 會影響所有實作的效能。 | 把重度運算抽成獨立函式,讓實作者自行決定是否呼叫。 |
| 過度依賴關聯型別 | 使特徵變得難以實作,特別是對外部型別。 | 僅在必要時使用 type Item,否則使用泛型參數。 |
忘記加上 where Self: Sized |
某些預設方法需要 Self 為具體型別(例如回傳 Self),否則編譯失敗。 |
在需要的預設方法前加上 where Self: Sized。 |
| 預設實作與未來版本衝突 | 新增預設方法後,舊有實作若不符合新行為,可能產生語意錯誤。 | 使用 語意版本化(semver),在大幅變更時提供 #[deprecated] 或新特徵。 |
最佳實踐:
- 保持預設方法簡潔:只做「輔助」或「組合」工作,避免在預設方法內寫大量邏輯。
- 使用文件說明:在特徵的文檔中明確指出哪些方法是「必須」實作、哪些是「可自行覆寫」。
- 提供測試範例:在庫的
tests/目錄放置特徵的預設行為測試,確保未來改動不會破壞相容性。 - 考慮未來擴充:若預計日後會加入新功能,先以預設方法的形式加入,讓使用者自行決定是否覆寫。
實際應用場景
日誌框架
trait Logger { fn log(&self, msg: &str); fn info(&self, msg: &str) { self.log(&format!("[INFO] {}", msg)) } }
使用者只需實作log,info、warn、error等級皆自動提供。資料庫抽象層
trait Repository<T> { fn insert(&self, item: T); fn find(&self, id: i64) -> Option<T>; fn delete(&self, id: i64) { /* 預設使用 find + remove */ } }
只要實作insert與find,刪除操作即可直接使用預設實作。序列化/反序列化
標準庫的serde::Serialize與Deserialize皆利用預設方法提供to_string、from_str等便利函式,讓自訂型別只要實作核心的serialize/deserialize即可。圖形 UI 元件
trait Widget { fn draw(&self); fn resize(&mut self, w: u32, h: u32) { /* 預設保持比例 */ } }
大多數元件只需要實作draw,而resize依需求自行覆寫。測試框架
trait TestCase { fn run(&self); fn name(&self) -> &str; fn description(&self) -> &str { "" } }
測試套件只要提供run與name,說明文字可使用預設空字串。
總結
- 預設方法 是 Rust 特徵中提升抽象層次、減少重複程式碼的關鍵工具。
- 它允許 模板方法 的寫法,使得只要實作少數核心抽象,就能自動獲得完整的行為集合。
- 使用時要注意 抽象方法的宣告、效能影響、以及 相容性,並遵循 簡潔、文件化、測試化 的最佳實踐。
- 在日誌、資料庫、序列化、UI、測試等多種實務領域,預設方法都能大幅提升開發效率與程式碼可維護性。
掌握了預設方法,你就能在 Rust 生態中寫出更具彈性、可擴充且易於維護的程式碼。祝你在 Rust 的旅程中玩得開心,寫出更好、更安全的程式!