枚舉(Enum)#

枚舉(Enum) 讓你定義一個型別,它的值只能是幾個固定的 變體(Variant) 之一。 Rust 的枚舉比許多語言更強大,因為每個變體可以攜帶不同型別的資料。


1. 定義枚舉#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
enum Direction {
    North,
    South,
    East,
    West,
}

fn main() {
    let dir = Direction::North;
}

2. 攜帶資料的枚舉#

每個變體可以攜帶不同型別和數量的資料:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
enum Shape {
    Circle(f64),           // 半徑
    Rectangle(f64, f64),   // 寬、高
    Triangle(f64, f64, f64), // 三邊
}

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()
        }
    }
}

3. 枚舉的方法#

和結構體一樣,枚舉也可以使用 impl 定義方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

impl Coin {
    fn value(&self) -> u32 {
        match self {
            Coin::Penny   => 1,
            Coin::Nickel  => 5,
            Coin::Dime    => 10,
            Coin::Quarter => 25,
        }
    }
}

fn main() {
    let c = Coin::Quarter;
    println!("面值:{} 分", c.value());
}

4. Option<T>#

許多語言用 null 表示「沒有值」,但 null 很容易在程式執行時突然炸掉(NullPointerException)。 Rust 完全沒有 null,改用 Option<T> 枚舉明確表達「值 可能存在、也可能不存在 」:

1
2
3
4
enum Option<T> {
    Some(T), // 有值,攜帶型別 T 的資料
    None,    // 沒有值
}

T 是泛型型別參數,代表實際存放的資料型別。例如 Option<i32> 表示可能有一個 i32Option<String> 表示可能有一個 String。泛型的完整說明見 CH14 泛型。

建立 Option#

1
2
3
4
5
6
let a: Option<i32> = Some(42); // 有值
let b: Option<i32> = None;     // 沒有值

// 型別通常可以推斷,不需要手動標注
let name: Option<&str> = Some("Alice");
let empty: Option<&str> = None;

為什麼要用 Option#

Rust 的型別系統把「可能沒有值」這件事 寫進型別裡 。 如果一個函數可能找不到結果,就回傳 Option<T>;如果確定一定有值,就直接回傳 T

這樣編譯器就能強制你在使用值之前,先處理「沒有值」的情況,而不是等到執行時才爆炸。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("Alice"))
    } else {
        None // 找不到時明確回傳 None
    }
}

fn main() {
    // 編譯器要求你處理 None,不能直接當 String 使用
    match find_user(1) {
        Some(name) => println!("找到用戶:{name}"),
        None       => println!("用戶不存在"),
    }
}

常用方法#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fn main() {
    let some_val: Option<i32> = Some(42);
    let no_val: Option<i32> = None;

    // unwrap_or:有值取值,沒值用預設值
    println!("{}", some_val.unwrap_or(0));  // 42
    println!("{}", no_val.unwrap_or(0));    // 0

    // unwrap_or_else:沒值時執行閉包產生預設值
    println!("{}", no_val.unwrap_or_else(|| 1 + 1)); // 2

    // is_some / is_none:檢查狀態
    println!("{}", some_val.is_some()); // true
    println!("{}", no_val.is_none());   // true

    // map:對 Some 內的值做轉換,None 原樣傳遞
    let doubled = some_val.map(|v| v * 2);
    println!("{:?}", doubled);          // Some(84)
    println!("{:?}", no_val.map(|v| v * 2)); // None

    // and_then:串接可能失敗的步驟(類似 ? 運算子)
    let result = some_val
        .and_then(|v| if v > 10 { Some(v) } else { None })
        .and_then(|v| Some(v.to_string()));
    println!("{:?}", result); // Some("42")

    // unwrap:取出 Some 的值,若是 None 則 panic(只在確定不會是 None 時使用)
    println!("{}", some_val.unwrap()); // 42
}

if let 簡化寫法#

只在乎有值的情況時,用 if letmatch 簡潔:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("Alice"))
    } else {
        None
    }
}

fn main() {
    let name = find_user(1);

    // match 寫法
    match name {
        Some(n) => println!("你好,{n}"),
        None    => {},
    }

    // if let 寫法(等效,更簡潔)
    if let Some(n) = find_user(1) {
        println!("你好,{n}");
    }
}

6. Result<T, E>#

Result<T, E> 是 Rust 處理錯誤的核心機制,用來表達 可能失敗的操作

1
2
3
4
enum Result<T, E> {
    Ok(T),  // 成功,攜帶型別 T 的結果值
    Err(E), // 失敗,攜帶型別 E 的錯誤值
}

TE 是泛型型別參數,由使用情境決定。例如讀取檔案時,TString(成功取得內容),Eio::Error(失敗的原因)。

建立 Result#

1
2
3
4
5
6
7
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("除數不能為零")) // 回傳錯誤
    } else {
        Ok(a / b)                         // 回傳成功值
    }
}

用 match 處理 Result#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("除數不能為零"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    match divide(10.0, 2.0) {
        Ok(result) => println!("結果:{result}"),   // 結果:5
        Err(msg)   => println!("錯誤:{msg}"),
    }

    match divide(10.0, 0.0) {
        Ok(result) => println!("結果:{result}"),
        Err(msg)   => println!("錯誤:{msg}"),      // 錯誤:除數不能為零
    }
}

常用方法#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fn main() {
    let ok: Result<i32, &str> = Ok(42);
    let err: Result<i32, &str> = Err("出錯了");

    // unwrap_or:成功取值,失敗用預設值
    println!("{}", ok.unwrap_or(0));  // 42
    println!("{}", err.unwrap_or(0)); // 0

    // is_ok / is_err:檢查狀態
    println!("{}", ok.is_ok());   // true
    println!("{}", err.is_err()); // true

    // map:對 Ok 內的值做轉換,Err 原樣傳遞
    let doubled = ok.map(|v| v * 2);
    println!("{:?}", doubled); // Ok(84)

    // unwrap:取出 Ok 值,若是 Err 則 panic(只在確定不會失敗時使用)
    println!("{}", ok.unwrap()); // 42
}

Result 與 Option 的差別#

Option<T>Result<T, E>
失敗變體None(無額外資訊)Err(E)(攜帶錯誤原因)
使用場景值可能不存在操作可能失敗,且需要知道原因
典型例子查詢 HashMap讀取檔案、解析字串

7. ? 運算子#

? 是專門用來傳遞錯誤的語法糖,只能用在回傳 ResultOption 的函數內。

用於 Result#

Result 值後面加 ?,效果是:

  • 若是 Ok(v):取出 v,繼續執行後面的程式碼
  • 若是 Err(e)立即結束函數 ,把 Err(e) 作為函數的回傳值
1
2
3
4
5
6
7
8
use std::fs;
use std::io;

fn read_file(path: &str) -> Result<String, io::Error> {
    let content = fs::read_to_string(path)?; // 若失敗,整個函數在此結束並回傳 Err
    let trimmed = content.trim().to_string(); // 只有成功時才會執行到這行
    Ok(trimmed)
}

等同於手動寫:

1
2
3
4
5
6
7
fn read_file(path: &str) -> Result<String, io::Error> {
    let content = match fs::read_to_string(path) {
        Ok(v)  => v,           // 成功:取出值繼續
        Err(e) => return Err(e), // 失敗:提前結束函數
    };
    Ok(content.trim().to_string())
}

串接多個可能失敗的步驟#

沒有 ? 時,每一步都要 match,程式碼層層嵌套:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn process(path: &str) -> Result<usize, io::Error> {
    match fs::read_to_string(path) {
        Err(e) => Err(e),
        Ok(content) => {
            match content.trim().parse::<usize>() {
                Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)),
                Ok(n)  => Ok(n * 2),
            }
        }
    }
}

? 後,每一步失敗就自動往上傳遞,程式碼變得線性易讀:

1
2
3
4
5
6
7
8
9
use std::fs;
use std::io;

fn process(path: &str) -> Result<usize, io::Error> {
    let content = fs::read_to_string(path)?;              // 步驟 1
    let n: usize = content.trim().parse()
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; // 步驟 2
    Ok(n * 2)                                             // 步驟 3
}

用於 Option#

? 也可以用在回傳 Option 的函數:

  • 若是 Some(v):取出 v 繼續執行
  • 若是 None立即結束函數 ,回傳 None
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn first_word(s: &str) -> Option<&str> {
    let words: Vec<&str> = s.split_whitespace().collect();
    let first = words.first()?; // 若 words 是空的(None),整個函數回傳 None
    Some(first)
}

fn main() {
    println!("{:?}", first_word("hello world")); // Some("hello")
    println!("{:?}", first_word(""));            // None
}

使用限制#

? 只能在函數回傳型別與 ? 作用的型別相符時使用:

1
2
3
4
fn main() {
    // let n: i32 = "42".parse()?; // 錯誤!main 回傳 (),不是 Result
    println!("? 只能用在回傳 Result 或 Option 的函數內");
}

若要在 main 中使用 ?,需要改變其回傳型別:

1
2
3
4
5
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let n: i32 = "42".parse()?; // parse 失敗時自動回傳 Err,現在可以了
    println!("{n}");
    Ok(())
}

Reference#

https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html