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

Rust struct 與 enum:對比 Python dataclass 與 Enum

·9 分鐘· loading · loading · ·
Rust Python Struct Enum Pattern-Matching Dataclass
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Rust 入門 - 本文屬於一個選集。
§ 3: 本文

一、前言
#

嗨,這裡是拍拍君!🦀

上一篇我們搞懂了 Ownership 與 Borrowing,Rust 編譯器已經不再對你咆哮了(大概)。現在是時候學怎麼組織資料了!

在 Python 裡,我們有 dataclass 來定義結構化資料,有 Enum 來定義一組固定的選項。Rust 也有對應的東西——structenum——但它們強大得多。

今天拍拍君要帶你看看 Rust 的 structenum 到底厲害在哪裡,特別是 enum + match 的組合,會讓你重新思考「資料建模」這件事。

二、Struct vs Dataclass:結構化資料
#

Python 的 dataclass
#

Python 3.7 引入了 dataclass,讓你不用手寫 __init__

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class User:
    name: str
    email: str
    age: int
    active: bool = True

# 使用
p = Point(3.0, 4.0)
print(p)  # Point(x=3.0, y=4.0)

user = User(name="拍拍君", email="pypy@example.com", age=1)
print(user.active)  # True

Python 的 dataclass 幫你自動生成:

  • __init__() — 建構子
  • __repr__() — 好看的印出
  • __eq__() — 比較
  • 還可選 frozen=True 來做不可變版本

Rust 的 struct
#

Rust 的 struct 乍看很像,但它是真正的型別,編譯期就確保所有欄位的型別正確:

struct Point {
    x: f64,
    y: f64,
}

struct User {
    name: String,
    email: String,
    age: u32,
    active: bool,
}

fn main() {
    let p = Point { x: 3.0, y: 4.0 };
    println!("({}, {})", p.x, p.y);

    let user = User {
        name: String::from("拍拍君"),
        email: String::from("pypy@example.com"),
        age: 1,
        active: true,
    };
    println!("{} is active: {}", user.name, user.active);
}

差異比較
#

特性 Python dataclass Rust struct
型別檢查 執行期(type hint 只是提示) 編譯期(強制)
預設值 field = value 要自己實作 Default trait
不可變 frozen=True 預設就不可變(要 mut
方法 直接在 class 裡寫 impl 區塊
繼承 可以 沒有繼承,用組合 + trait

幫 struct 加方法
#

Python 直接在 class 裡寫方法,Rust 用 impl 區塊:

# Python
@dataclass
class Circle:
    radius: float

    def area(self) -> float:
        return 3.14159 * self.radius ** 2

    def scale(self, factor: float) -> "Circle":
        return Circle(self.radius * factor)

c = Circle(5.0)
print(c.area())       # 78.53975
print(c.scale(2.0))   # Circle(radius=10.0)
struct Circle {
    radius: f64,
}

impl Circle {
    // 方法:第一個參數是 &self
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }

    // 回傳新的 Circle
    fn scale(&self, factor: f64) -> Circle {
        Circle {
            radius: self.radius * factor,
        }
    }

    // 關聯函式(類似 Python 的 classmethod / staticmethod)
    fn unit() -> Circle {
        Circle { radius: 1.0 }
    }
}

fn main() {
    let c = Circle { radius: 5.0 };
    println!("面積:{:.2}", c.area());    // 面積:78.54
    let c2 = c.scale(2.0);
    println!("半徑:{}", c2.radius);      // 半徑:10

    // 關聯函式用 :: 呼叫(不是 .)
    let unit = Circle::unit();
    println!("單位圓半徑:{}", unit.radius); // 1
}

注意:Circle::unit():: 呼叫,因為它不需要 self——就像 Python 的 @staticmethod

自動產生 Debug / PartialEq
#

Python 的 dataclass 自動給你 __repr____eq__。Rust 要手動加 derive

#[derive(Debug, PartialEq, Clone)]
struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let p1 = Point { x: 1.0, y: 2.0 };
    let p2 = Point { x: 1.0, y: 2.0 };

    println!("{:?}", p1);      // Point { x: 1.0, y: 2.0 }
    println!("{}", p1 == p2);  // true

    let p3 = p1.clone();      // 因為 derive(Clone)
    println!("{:?}", p3);
}

#[derive(...)] 就像 Python dataclass 幫你自動生成的那些 dunder methods,但你可以精確控制要哪些。

三、Enum:這才是 Rust 的大殺器
#

Python 的 Enum
#

Python 的 Enum 很直覺——就是一組命名的常數:

from enum import Enum, auto

class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()

class Direction(Enum):
    NORTH = "N"
    SOUTH = "S"
    EAST = "E"
    WEST = "W"

# 使用
print(Color.RED)           # Color.RED
print(Color.RED.value)     # 1
print(Direction.NORTH.value)  # "N"

# 比較
if Color.RED == Color.RED:
    print("相等!")

Python 的 Enum 就是「一組標籤」,每個標籤可以帶一個值。簡單、好用,但功能到這裡就差不多了。

Rust 的 enum:會帶資料的變體
#

Rust 的 enum 完全是另一個級別。每個變體(variant)可以帶不同結構的資料:

// 基本 enum — 跟 Python 一樣
#[derive(Debug)]
enum Color {
    Red,
    Green,
    Blue,
}

// 進階 enum — 每個變體帶不同的資料!
#[derive(Debug)]
enum Shape {
    Circle(f64),                    // 帶一個半徑
    Rectangle(f64, f64),            // 帶寬和高
    Triangle { a: f64, b: f64, c: f64 }, // 帶三個邊(具名欄位)
}

fn main() {
    let s1 = Shape::Circle(5.0);
    let s2 = Shape::Rectangle(3.0, 4.0);
    let s3 = Shape::Triangle { a: 3.0, b: 4.0, c: 5.0 };

    println!("{:?}", s1);  // Circle(5.0)
    println!("{:?}", s2);  // Rectangle(3.0, 4.0)
    println!("{:?}", s3);  // Triangle { a: 3.0, b: 4.0, c: 5.0 }
}

在 Python 你要這樣才能做到類似的事:

from dataclasses import dataclass
from typing import Union

@dataclass
class Circle:
    radius: float

@dataclass
class Rectangle:
    width: float
    height: float

@dataclass
class Triangle:
    a: float
    b: float
    c: float

# 用 Union 或 type alias
Shape = Union[Circle, Rectangle, Triangle]

# 但 Python 不會在編譯期幫你檢查有沒有處理所有情況!

看到差異了嗎?Python 需要多個 class + Union,而 Rust 一個 enum 就搞定,而且編譯器會確保你處理了每一種情況。

四、Pattern Matching:match 的威力
#

Python 的 match(3.10+)
#

Python 3.10 加入了 structural pattern matching:

def describe_shape(shape: Shape) -> str:
    match shape:
        case Circle(radius=r):
            return f"圓形,半徑 {r}"
        case Rectangle(width=w, height=h):
            return f"長方形,{w} x {h}"
        case Triangle(a=a, b=b, c=c):
            return f"三角形,邊長 {a}, {b}, {c}"
        case _:
            return "未知形狀"  # 需要 catch-all

c = Circle(5.0)
print(describe_shape(c))  # 圓形,半徑 5.0

Python 的 match 很方便,但問題是:如果你新增了一種形狀卻忘了加 case,Python 不會告訴你。它就默默走進 case _ 或直接略過。

Rust 的 match:窮舉檢查
#

#[derive(Debug)]
enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
    Triangle { a: f64, b: f64, c: f64 },
}

fn describe(shape: &Shape) -> String {
    match shape {
        Shape::Circle(r) => format!("圓形,半徑 {}", r),
        Shape::Rectangle(w, h) => format!("長方形,{} x {}", w, h),
        Shape::Triangle { a, b, c } => {
            format!("三角形,邊長 {}, {}, {}", a, b, c)
        }
    }
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle(w, h) => w * h,
        Shape::Triangle { a, b, c } => {
            // 海龍公式
            let s = (a + b + c) / 2.0;
            (s * (s - a) * (s - b) * (s - c)).sqrt()
        }
    }
}

fn main() {
    let shapes = vec![
        Shape::Circle(5.0),
        Shape::Rectangle(3.0, 4.0),
        Shape::Triangle { a: 3.0, b: 4.0, c: 5.0 },
    ];

    for shape in &shapes {
        println!("{} → 面積 {:.2}", describe(shape), area(shape));
    }
}

輸出:

圓形,半徑 5 → 面積 78.54
長方形,3 x 4 → 面積 12.00
三角形,邊長 3, 4, 5 → 面積 6.00

最關鍵的部分:如果你在 Shape enum 新增了一個變體,例如 Pentagon(f64),但忘了在 match 裡處理它——編譯器會直接報錯

error[E0004]: non-exhaustive patterns: `Shape::Pentagon(_)` not covered

這就是 Rust 的窮舉檢查(exhaustiveness checking)。Python 做不到這件事。

五、Option 與 Result:Rust 最常見的 enum
#

Rust 標準庫裡有兩個你每天都會用到的 enum:

Option:取代 None
#

Python 的 None 是萬惡之源。任何變數都可能是 None,但你只有在跑到那一行才知道:

# Python:None 的恐怖
def find_user(name: str) -> dict | None:
    users = {"pypy": {"age": 1}}
    return users.get(name)

user = find_user("maho")
# user 是 None,但如果你忘了檢查...
print(user["age"])  # 💥 TypeError: 'NoneType' is not subscriptable

Rust 用 Option<T> 來表達「可能沒有值」:

fn find_user(name: &str) -> Option<u32> {
    match name {
        "pypy" => Some(1),
        _ => None,
    }
}

fn main() {
    let user = find_user("maho");

    // 你不能直接用 user——因為它是 Option,不是 u32
    // println!("{}", user);  // ❌ 編譯錯誤

    // 必須處理 None 的情況
    match user {
        Some(age) => println!("找到了!年齡:{}", age),
        None => println!("找不到這個使用者"),
    }

    // 或用更簡潔的方式
    if let Some(age) = find_user("pypy") {
        println!("拍拍君的年齡:{}", age);
    }

    // unwrap_or 提供預設值
    let age = find_user("maho").unwrap_or(0);
    println!("預設年齡:{}", age);
}

Result<T, E>:取代 try/except
#

Python 用例外處理錯誤,但你永遠不知道一個函式會丟什麼例外:

# Python:例外可能從天而降
def read_number(s: str) -> int:
    return int(s)  # 可能丟 ValueError,但函式簽名看不出來

try:
    n = read_number("abc")
except ValueError:
    n = 0

Rust 用 Result<T, E>,把錯誤寫進型別:

fn read_number(s: &str) -> Result<i32, String> {
    s.parse::<i32>()
        .map_err(|e| format!("解析失敗:{}", e))
}

fn main() {
    // 必須處理成功和失敗兩種情況
    match read_number("42") {
        Ok(n) => println!("數字是 {}", n),
        Err(e) => println!("錯誤:{}", e),
    }

    // Result 也有很多方便的方法
    let n = read_number("abc").unwrap_or(0);
    println!("預設:{}", n);  // 預設:0

    // ? 運算子:自動傳播錯誤(類似 Python 的不 catch 讓例外往上丟)
    // 需要在回傳 Result 的函式裡使用
}

OptionResult 的哲學:把可能的失敗寫進型別系統,讓編譯器強迫你處理。不再有「忘了 try/except」或「忘了檢查 None」的 bug。

六、? 運算子:優雅的錯誤傳播
#

Python 的 try/except 可以讓例外自動往上層傳播。Rust 的 ? 運算子做類似的事,但更明確:

use std::fs;
use std::io;

// ? 會自動把 Err 回傳給呼叫者
fn read_config(path: &str) -> Result<String, io::Error> {
    let content = fs::read_to_string(path)?;  // 失敗就立刻回傳 Err
    Ok(content.trim().to_string())
}

fn get_port(path: &str) -> Result<u16, Box<dyn std::error::Error>> {
    let content = read_config(path)?;
    let port = content.parse::<u16>()?;  // 解析失敗也用 ?
    Ok(port)
}

fn main() {
    match get_port("config.txt") {
        Ok(port) => println!("Port: {}", port),
        Err(e) => println!("設定讀取失敗:{}", e),
    }
}

對比 Python:

def read_config(path: str) -> str:
    return open(path).read().strip()  # 可能丟 FileNotFoundError

def get_port(path: str) -> int:
    content = read_config(path)  # 可能丟 FileNotFoundError
    return int(content)          # 可能丟 ValueError

try:
    port = get_port("config.txt")
    print(f"Port: {port}")
except (FileNotFoundError, ValueError) as e:
    print(f"設定讀取失敗:{e}")

Rust 的 ? 讓錯誤處理跟 Python 的例外傳播一樣簡潔,但每一步都是明確的——函式簽名就告訴你「這裡可能失敗」。

七、實戰:用 enum 建模一個簡單的命令系統
#

最後來看一個完整的例子。假設我們在做一個任務管理系統:

Python 版本
#

from dataclasses import dataclass
from enum import Enum, auto

class Priority(Enum):
    LOW = auto()
    MEDIUM = auto()
    HIGH = auto()

@dataclass
class Task:
    title: str
    priority: Priority
    done: bool = False

# 命令用 dict 或 Union 模擬
def execute(command: dict, tasks: list[Task]):
    match command:
        case {"action": "add", "title": t, "priority": p}:
            tasks.append(Task(t, Priority[p]))
            print(f"新增:{t}")
        case {"action": "done", "index": i}:
            tasks[i].done = True
            print(f"完成:{tasks[i].title}")
        case {"action": "list"}:
            for i, t in enumerate(tasks):
                status = "✅" if t.done else "⬜"
                print(f"  {i}. {status} [{t.priority.name}] {t.title}")
        case _:
            print("未知命令")

tasks: list[Task] = []
execute({"action": "add", "title": "學 Rust", "priority": "HIGH"}, tasks)
execute({"action": "add", "title": "寫部落格", "priority": "MEDIUM"}, tasks)
execute({"action": "done", "index": 0}, tasks)
execute({"action": "list"}, tasks)

Rust 版本
#

#[derive(Debug, Clone)]
enum Priority {
    Low,
    Medium,
    High,
}

#[derive(Debug, Clone)]
struct Task {
    title: String,
    priority: Priority,
    done: bool,
}

// 命令也是 enum!每個變體帶不同的資料
#[derive(Debug)]
enum Command {
    Add { title: String, priority: Priority },
    Done { index: usize },
    List,
}

fn execute(command: &Command, tasks: &mut Vec<Task>) {
    match command {
        Command::Add { title, priority } => {
            tasks.push(Task {
                title: title.clone(),
                priority: priority.clone(),
                done: false,
            });
            println!("新增:{}", title);
        }
        Command::Done { index } => {
            if let Some(task) = tasks.get_mut(*index) {
                task.done = true;
                println!("完成:{}", task.title);
            } else {
                println!("索引 {} 不存在", index);
            }
        }
        Command::List => {
            for (i, task) in tasks.iter().enumerate() {
                let status = if task.done { "✅" } else { "⬜" };
                println!(
                    "  {}. {} [{:?}] {}",
                    i, status, task.priority, task.title
                );
            }
        }
    }
}

fn main() {
    let mut tasks: Vec<Task> = Vec::new();

    let commands = vec![
        Command::Add {
            title: String::from("學 Rust"),
            priority: Priority::High,
        },
        Command::Add {
            title: String::from("寫部落格"),
            priority: Priority::Medium,
        },
        Command::Done { index: 0 },
        Command::List,
    ];

    for cmd in &commands {
        execute(cmd, &mut tasks);
    }
}

輸出:

新增:學 Rust
新增:寫部落格
完成:學 Rust
  0. ✅ [High] 學 Rust
  1. ⬜ [Medium] 寫部落格

Rust 版本的優勢:

  1. Command enum — 所有可能的命令都在型別裡定義好了,不是一個任意的 dict
  2. 窮舉檢查 — 新增 Command::Delete 變體時,編譯器會逼你處理
  3. 型別安全 — 不會把 “HIGH” 字串打成 “HGIH” 然後跑到一半爆掉

八、整理比較
#

概念 Python Rust
結構化資料 @dataclass struct
方法 直接在 class 裡 impl 區塊
列舉 enum.Enum(只帶值) enum(帶不同結構的資料)
模式匹配 match(3.10+,不強制窮舉) match(強制窮舉)
空值 None(任何地方出現) Option<T>(型別明確)
錯誤處理 try/except(隱式) Result<T, E> + ?(顯式)
繼承 支援 不支援(用 trait + 組合)

九、小結
#

拍拍君覺得 Rust 的 enum 是從 Python 轉過來最讓人「哇」的功能。一開始你會覺得「enum 不就是列舉嗎」,但當你發現每個變體可以帶不同的資料,再配上 match 的窮舉檢查——你會發現 Python 裡一堆用 if isinstance() 東拼西湊的程式碼,在 Rust 裡都有更優雅的解法。

OptionResult 也是同樣的道理:不是 Rust 不讓你犯錯,是它把「可能出錯的地方」寫進型別系統裡,讓編譯器在你跑程式之前就幫你抓到問題。

下一篇我們會回到 Python 主題,看看 multiprocessing 怎麼突破 GIL 的限制!

Happy coding!🦀✨

延伸閱讀
#

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

相關文章

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
Polars:比 Pandas 快 10 倍的 DataFrame 新選擇
·6 分鐘· loading · loading
Python Polars Dataframe 資料分析 Rust
超快速 Python 套件管理:uv 完全教學
·6 分鐘· loading · loading
Python Uv Package Manager Rust
Python subprocess:外部命令執行與管道串接完全指南
·8 分鐘· loading · loading
Python Subprocess Shell Automation Cli
Python 裝飾器:讓你的函式穿上超能力外套
·7 分鐘· loading · loading
Python Decorator 裝飾器 進階語法 設計模式