快轉到主要內容
  1. 教學文章/

Rust trait vs Python ABC/Protocol:抽象介面大比拼

·8 分鐘· loading · loading · ·
Rust Python Trait Abc Protocol 泛型 Interface
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Rust 入門 - 本文屬於一個選集。
§ 5: 本文

一. 前言
#

寫 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 不只是介面,它是整個型別系統的核心。從基本的 DisplayDebug 到進階的泛型約束、extension trait、關聯型別,幾乎所有 Rust 的「魔法」都建立在 trait 之上。

如果你是 Python 工程師,最大的心態轉換是:不再依賴 duck typing,改成在編譯期就把介面契約定死。 一開始可能覺得麻煩,但享受到「編譯通過就能跑」的安心感後,就回不去了 😄

📖 下一篇我們會繼續探索 Rust 的更多概念。記得追蹤「Rust 入門」系列!


延伸閱讀
#

Rust 入門 - 本文屬於一個選集。
§ 5: 本文

相關文章

Rust 錯誤處理:Result/Option vs Python try/except
·7 分鐘· loading · loading
Rust Python Error-Handling Result Option
Rust for Python 開發者:Ownership 與 Borrowing 入門
·7 分鐘· loading · loading
Rust Python Ownership Borrowing Memory
Rust CLI 實戰:用 clap 打造命令列工具(Python Typer 對照版)
·5 分鐘· loading · loading
Rust Cli Clap Typer Python
Rust struct 與 enum:對比 Python dataclass 與 Enum
·9 分鐘· loading · loading
Rust Python Struct Enum Pattern-Matching Dataclass
Polars:比 Pandas 快 10 倍的 DataFrame 新選擇
·6 分鐘· loading · loading
Python Polars Dataframe 資料分析 Rust
超快速 Python 套件管理:uv 完全教學
·6 分鐘· loading · loading
Python Uv Package Manager Rust