一、前言 #
嗨,這裡是拍拍君!🦀
上一篇我們搞懂了 Ownership 與 Borrowing,Rust 編譯器已經不再對你咆哮了(大概)。現在是時候學怎麼組織資料了!
在 Python 裡,我們有 dataclass 來定義結構化資料,有 Enum 來定義一組固定的選項。Rust 也有對應的東西——struct 和 enum——但它們強大得多。
今天拍拍君要帶你看看 Rust 的 struct 和 enum 到底厲害在哪裡,特別是 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 的函式裡使用
}
Option 和 Result 的哲學:把可能的失敗寫進型別系統,讓編譯器強迫你處理。不再有「忘了 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 版本的優勢:
Commandenum — 所有可能的命令都在型別裡定義好了,不是一個任意的 dict- 窮舉檢查 — 新增
Command::Delete變體時,編譯器會逼你處理 - 型別安全 — 不會把 “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 裡都有更優雅的解法。
Option 和 Result 也是同樣的道理:不是 Rust 不讓你犯錯,是它把「可能出錯的地方」寫進型別系統裡,讓編譯器在你跑程式之前就幫你抓到問題。
下一篇我們會回到 Python 主題,看看 multiprocessing 怎麼突破 GIL 的限制!
Happy coding!🦀✨