2.2 型別系統、Trait 與泛型:多型之三種形態#
型別即規格#
靜態型別系統的本質是以機器可檢查的方式寫下程式的規格。型別越精確,規格越完整,編譯器能捕捉的錯誤越多。Rust 的型別系統擁有現代語言中幾乎全套的武器:
- 代數資料型別(ADT):enum + struct
- Trait:類似 typeclass 的多型約束
- 泛型與單態化:零成本的編譯期多型
- 型別狀態(typestate):以型別編碼狀態機
代數資料型別:和與積#
\[ \text{Sum: } T = A + B \quad(\text{enum,值屬於其中一種}) \]
\[ \text{Product: } T = A \times B \quad(\text{struct,同時持有全部欄位}) \]
以 ADT 精確建模領域,是 Rust 開發的核心功夫。例:一個使用者帳號有三種狀態:
enum AccountState {
Pending { email: String }, // 未驗證:只有 email
Active { email: String, plan: Plan },
Suspended { reason: String },
}
對比 JavaScript 常見的寫法(status 字串 + 可選欄位),ADT 讓「Pending 狀態下不可能有 plan」成為編譯期事實。這叫讓非法狀態不可表示(make illegal states unrepresentable)。
Trait:編譯期多型#
Trait 定義行為契約,類似 Haskell typeclass 或 Java interface,但有三點關鍵差異:
- 可以為外部型別實作自己的 trait(孤兒規則管理一致性)
- 泛型約束在編譯期單態化,無虛函數表開銷
- trait 可以有關聯型別與關聯常數
trait Repository {
type Item;
type Error;
async fn find(&self, id: Uuid) -> Result<Option<Self::Item>, Self::Error>;
}
多型的三種形態與成本模型#
| 形態 | 機制 | 成本 | 適用 |
|---|---|---|---|
泛型(fn f<T: Repo>) | 單態化 | 零執行期成本,編譯變慢、體積變大 | 熱路徑 |
Trait 物件(dyn Repo) | 虛表動態分派 | 一次間接呼叫,無法內聯 | 需執行期多型 |
| Enum 分派 | match | 最快,但型別集合封閉 | 型別集合已知 |
選擇法則:型別集合編譯期已知 → enum;熱路徑泛型抽象 → 泛型;需要插件式擴展 → dyn。
flowchart TD
A[需要多型] --> B{型別集合固定?}
B -->|是| C[enum + match]
B -->|否| D{效能關鍵?}
D -->|是| E[泛型 單態化]
D -->|否| F[dyn trait 物件]泛型與單態化#
fn to_json<T: Serialize>(item: &T) -> String {
serde_json::to_string(item).unwrap()
}
// 編譯器為每個用到的 T 生成一份專屬程式碼:
// to_json::<User>, to_json::<Order>, ...
這是 C++ 模板與 Haskell 型別類的混合體。代價是編譯時間與二進位體積,收益是零成本抽象 + 完整內聯最佳化。
型別狀態模式:把狀態機寫進型別#
Web 開發中的經典需求:只有「已取得連線」的物件才能發查詢;只有「已驗證」的請求才能進入受保護的路由。
struct Unverified;
struct Verified;
struct Request<State> {
body: String,
_state: PhantomData<State>,
}
impl Request<Unverified> {
fn verify(self, token: &str) -> Option<Request<Verified>> { /* ... */ }
}
impl Request<Verified> {
fn process(self) { /* 只有 Verified 能呼叫 */ }
}
呼叫 Request::<Unverified>::process() 直接編譯失敗 —— 狀態轉移規則被編碼進型別系統,編譯器成為狀態機的驗證者。
Serde:序列化即型別推導#
本書前後端通訊全賴 serde(serialize/deserialize):
#[derive(Serialize, Deserialize)]
struct User {
id: Uuid,
name: String,
#[serde(skip_serializing)]
password_hash: String, // 永不出現在 JSON 中
}
一次 derive,同時獲得 JSON 序列化與反序列化,且欄位與型別由編譯器保證與 struct 一致。這是 4.4 節型別共享管道的基礎。
習題#
- 用 ADT 重新設計「訂單狀態:待付款/已付款/已出貨/已取消」,寫出 Rust enum,並說明哪些非法轉移被型別擋住。
- 何時應該用
dyn Trait而非泛型?舉一個 Web 應用中的實例。 - 型別狀態模式如何用於「資料庫交易」的型別建模(未提交前不能查新資料)?