一. 前言 #
寫 Python 寫久了,大家對「介面」這個概念應該不陌生。你可能用過 ABC(Abstract Base Class)來強制子類實作某些方法,或是用比較新潮的 Protocol 來做結構化子型別(structural subtyping)。
但在 Rust 的世界裡,trait 是一切抽象的基石。它不只是「介面」,還能做泛型約束、預設實作、運算子多載、甚至是條件式方法實作。可以說,搞懂 trait 就搞懂了 Rust 大半的型別系統。
今天拍拍君就帶大家從 Python 的角度出發,看看 Rust 的 trait 到底強在哪裡!
📌 這篇是「Rust 入門」系列第五篇。如果你還沒看過前幾篇,建議先從 Rust for Pythonistas 開始!
二. Python 的介面:ABC vs Protocol #
在正式進入 Rust 之前,先快速複習一下 Python 的兩種介面機制。
2.1 ABC(Abstract Base Class) #
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
"""計算面積"""
...
@abstractmethod
def perimeter(self) -> float:
"""計算周長"""
...
def describe(self) -> str:
"""預設實作:不用覆寫也行"""
return f"面積={self.area():.2f}, 周長={self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
def perimeter(self) -> float:
return 2 * 3.14159 * self.radius
circle = Circle(5.0)
print(circle.describe()) # 面積=78.54, 周長=31.42
ABC 的特色:
- 繼承式(nominal)——必須
class Circle(Shape)明確繼承 - 忘了實作
abstractmethod會在實例化時 runtime 報錯 - 支援預設實作(
describe)
2.2 Protocol(Python 3.8+) #
from typing import Protocol
class Drawable(Protocol):
def draw(self, canvas: str) -> None: ...
class Button:
"""不需要繼承 Drawable,只要有 draw 方法就行"""
def draw(self, canvas: str) -> None:
print(f"在 {canvas} 上畫一顆按鈕")
def render(widget: Drawable) -> None:
widget.draw("main_canvas")
render(Button()) # ✅ 型別檢查通過
Protocol 的特色:
- 結構式(structural)——不需要繼承,方法簽名對了就好
- 只在靜態型別檢查器(mypy / pyright)生效
- 更像 Go 的 interface 哲學
三. Rust 的 trait:基礎語法 #
好的,現在來看 Rust 怎麼做。
3.1 定義一個 trait #
trait Shape {
fn area(&self) -> f64;
fn perimeter(&self) -> f64;
// 預設實作(跟 ABC 的非 abstract 方法很像)
fn describe(&self) -> String {
format!("面積={:.2}, 周長={:.2}", self.area(), self.perimeter())
}
}
看起來跟 Python 的 ABC 很像對吧?但關鍵差異馬上就來了。
3.2 為型別實作 trait #
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn perimeter(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radius
}
// describe() 自動繼承預設實作,不需要寫
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
// 覆寫預設實作
fn describe(&self) -> String {
format!("長方形 {}x{}, 面積={:.2}", self.width, self.height, self.area())
}
}
注意語法:impl Shape for Circle——明確告訴編譯器「Circle 實作了 Shape」。
3.3 來用用看 #
fn main() {
let c = Circle { radius: 5.0 };
let r = Rectangle { width: 3.0, height: 4.0 };
println!("{}", c.describe()); // 面積=78.54, 周長=31.42
println!("{}", r.describe()); // 長方形 3x4, 面積=12.00
}
四. 比較表:三種介面機制 #
先來一張清楚的比較表,讓大家一目瞭然:
| 特性 | Python ABC | Python Protocol | Rust trait |
|---|---|---|---|
| 綁定方式 | 繼承(nominal) | 結構式(structural) | 明確 impl(nominal) |
| 檢查時機 | Runtime(實例化) | 靜態(mypy/pyright) | 編譯期 |
| 預設實作 | ✅ | ❌(通常不放) | ✅ |
| 泛型約束 | ❌ | 有限 | ✅ 超強 |
| 多重實作 | 多重繼承 | N/A | 多個 trait 自由組合 |
| 孤兒規則 | 無 | 無 | 有(後面會講) |
重點差異:
- Rust 的 trait 在編譯期就確認,忘了實作直接編譯失敗——不會像 Python ABC 跑到一半才炸
- Rust 的 trait 可以當泛型約束用,這是 Python 很難做到的
五. Trait 當泛型約束(Trait Bounds) #
這是 Rust trait 最強大的用途之一。
5.1 基本語法 #
// 方法一:impl Trait 語法(簡潔)
fn print_area(shape: &impl Shape) {
println!("面積:{:.2}", shape.area());
}
// 方法二:泛型 + where 語法(彈性)
fn print_info<T>(shape: &T)
where
T: Shape + std::fmt::Debug,
{
println!("{:?} 的面積:{:.2}", shape, shape.area());
}
Python 的等價寫法:
def print_area(shape: Shape) -> None: # ABC 版本
print(f"面積:{shape.area():.2f}")
def print_area(shape: Drawable) -> None: # Protocol 版本
shape.draw("canvas")
看起來差不多?但 Rust 的版本有一個殺手級優勢——零成本抽象。
5.2 零成本抽象(Zero-Cost Abstraction) #
fn total_area(shapes: &[&dyn Shape]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
// 或者用泛型版本(靜態分派,更快)
fn total_area_static<T: Shape>(shapes: &[T]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
泛型版本在編譯時會針對每個具體型別產生專用程式碼(monomorphization),跑起來跟直接呼叫沒有差別。而 dyn Shape 是動態分派,像 Python 一樣走虛擬函式表(vtable)。
💡 拍拍君小提醒:
impl Trait是靜態分派,dyn Trait是動態分派。大多數時候用impl Trait就好,效能最佳!
六. 多個 Trait 組合 #
在 Python 裡,多重繼承是出了名的複雜(MRO、diamond problem…)。Rust 用 trait 組合來解決:
6.1 一個型別可以實作多個 trait #
use std::fmt;
trait Printable {
fn to_string_pretty(&self) -> String;
}
trait Serializable {
fn to_json(&self) -> String;
}
struct User {
name: String,
age: u32,
}
impl Printable for User {
fn to_string_pretty(&self) -> String {
format!("{} ({}歲)", self.name, self.age)
}
}
impl Serializable for User {
fn to_json(&self) -> String {
format!(r#"{{"name":"{}","age":{}}}"#, self.name, self.age)
}
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.to_string_pretty())
}
}
6.2 要求多個 trait 約束 #
fn save_and_print<T: Printable + Serializable>(item: &T) {
println!("顯示:{}", item.to_string_pretty());
println!("JSON:{}", item.to_json());
}
fn main() {
let user = User {
name: String::from("拍拍君"),
age: 3,
};
save_and_print(&user);
}
Python 的等價做法需要用多重繼承或 intersection Protocol,相當彆扭:
class PrintableAndSerializable(Printable, Serializable, Protocol):
... # 😅 有點醜
七. 進階用法 #
7.1 為既有型別加方法(Extension Trait) #
這是 Rust trait 最「哇」的功能之一——你可以為任何型別(包括標準庫的)加上新方法!
trait StringExt {
fn is_palindrome(&self) -> bool;
}
impl StringExt for str {
fn is_palindrome(&self) -> bool {
let chars: Vec<char> = self.chars().collect();
let len = chars.len();
for i in 0..len / 2 {
if chars[i] != chars[len - 1 - i] {
return false;
}
}
true
}
}
fn main() {
println!("{}", "racecar".is_palindrome()); // true
println!("{}", "拍拍".is_palindrome()); // true! 😆
println!("{}", "hello".is_palindrome()); // false
}
Python 想做到同樣的事,只能用猴子補丁(monkey patching),而且 type checker 不會認:
# Python: 只能這樣硬塞... 🐒
str.is_palindrome = lambda s: s == s[::-1]
"racecar".is_palindrome() # mypy: 😡
7.2 孤兒規則(Orphan Rule) #
Rust 有一個限制:你只能為「自己的型別」或「自己的 trait」實作,不能為別人的型別實作別人的 trait。
// ✅ OK:自己的 trait + 標準庫的型別
impl StringExt for str { ... }
// ✅ OK:標準庫的 trait + 自己的型別
impl fmt::Display for User { ... }
// ❌ 不行:標準庫的 trait + 標準庫的型別
impl fmt::Display for Vec<i32> { ... } // 編譯錯誤!
這看似限制很大,但它防止了不同 crate 對同一個型別有衝突的實作——在大型專案裡這是救命的。
7.3 關聯型別(Associated Types) #
trait 裡除了方法,還可以定義型別:
trait Iterator {
type Item; // 關聯型別
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter {
count: u32,
max: u32,
}
impl Iterator for Counter {
type Item = u32; // 指定 Item 是 u32
fn next(&mut self) -> Option<u32> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
Python 的 Iterator protocol 用 __next__ 方法回傳 Any,完全不管型別。Rust 的關聯型別讓編譯器確切知道 next() 會回傳什麼。
八. 常用標準 Trait 速查 #
Rust 標準庫有很多內建 trait,對應 Python 的各種魔術方法:
| Rust Trait | Python 等價 | 功能 |
|---|---|---|
Display |
__str__ |
格式化顯示 |
Debug |
__repr__ |
除錯用顯示 |
Clone |
copy.copy |
深拷貝 |
PartialEq |
__eq__ |
相等比較 |
PartialOrd |
__lt__, __gt__ |
大小比較 |
Iterator |
__iter__ + __next__ |
迭代器 |
From / Into |
__init__ 多載 |
型別轉換 |
Default |
類別屬性預設值 | 預設值建構 |
Drop |
__del__ |
解構 / 釋放資源 |
Add, Mul |
__add__, __mul__ |
運算子多載 |
很多可以用 #[derive(...)] 自動產生:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
// 自動產生 Debug、Clone、PartialEq 的實作!
Python 的 @dataclass 也有類似效果:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# 自動產生 __repr__, __eq__ 等
九. 實戰:建立一個 Plugin 系統 #
來個綜合練習——用 trait 建立一個簡單的 plugin 系統:
trait Plugin {
fn name(&self) -> &str;
fn version(&self) -> &str;
fn execute(&self, input: &str) -> String;
fn info(&self) -> String {
format!("{} v{}", self.name(), self.version())
}
}
struct UpperPlugin;
struct ReversePlugin;
impl Plugin for UpperPlugin {
fn name(&self) -> &str { "upper" }
fn version(&self) -> &str { "1.0" }
fn execute(&self, input: &str) -> String {
input.to_uppercase()
}
}
impl Plugin for ReversePlugin {
fn name(&self) -> &str { "reverse" }
fn version(&self) -> &str { "0.2" }
fn execute(&self, input: &str) -> String {
input.chars().rev().collect()
}
}
fn run_plugins(plugins: &[&dyn Plugin], input: &str) {
for plugin in plugins {
println!("[{}] {} → {}", plugin.info(), input, plugin.execute(input));
}
}
fn main() {
let plugins: Vec<&dyn Plugin> = vec![&UpperPlugin, &ReversePlugin];
run_plugins(&plugins, "Hello 拍拍");
// [upper v1.0] Hello 拍拍 → HELLO 拍拍
// [reverse v0.2] Hello 拍拍 → 拍拍 olleH
}
這裡用 &dyn Plugin(動態分派)讓不同型別的 plugin 放在同一個 Vec 裡。Python 因為是動態型別,天生就能做到;Rust 需要明確選擇用動態分派。
結語 #
今天我們比較了三種「介面」機制:
- Python ABC:繼承式,runtime 檢查,適合傳統 OOP
- Python Protocol:結構式,靜態檢查,更彈性
- Rust trait:編譯期保證 + 零成本抽象 + 泛型約束 = 💪
Rust 的 trait 不只是介面,它是整個型別系統的核心。從基本的 Display、Debug 到進階的泛型約束、extension trait、關聯型別,幾乎所有 Rust 的「魔法」都建立在 trait 之上。
如果你是 Python 工程師,最大的心態轉換是:不再依賴 duck typing,改成在編譯期就把介面契約定死。 一開始可能覺得麻煩,但享受到「編譯通過就能跑」的安心感後,就回不去了 😄
📖 下一篇我們會繼續探索 Rust 的更多概念。記得追蹤「Rust 入門」系列!
延伸閱讀 #
- 📖 The Rust Book - Traits — 官方教程的 trait 章節
- 📖 Rust By Example - Traits — 範例導向學習
- 🐍 Python typing.Protocol 文件 — Protocol 的完整說明
- 📝 Rust struct 與 enum — 本系列上一篇,搭配閱讀更好懂