iT邦幫忙

2024 iThome 鐵人賽

DAY 9
1

在 Rust 中,枚舉(enum) 是一種強大且靈活的數據結構,允許你定義一個變數,其值可以是多種可能的狀態或類型之一。枚舉非常適合處理多種選項、狀態、錯誤處理和複雜數據結構。在這篇文章中,我們將介紹 Rust 中的枚舉是如何運作的,並展示它們的使用情境,最後會與 Python 的枚舉進行比較。


一、什麼是枚舉?

枚舉是一種類別,允許你定義一組「變體」,在 Rust 的枚舉中,「變體」(Variant)可以視為這個枚舉類別中的不同「選項」或「可能的狀態」。

舉個簡單的例子是,枚舉就像是鹹酥雞攤的菜單,然後裡面的每個變體就是各項商品,其中包括有各種類型,如澱粉類、肉類、蔬菜類還有豆製品類,相當於各種類別的資料格式,然後這些商品你也都可以選擇要辣不辣要切不切等狀態,也就是每種變體都可以再攜帶參數。因此顧名思義,枚舉可以是包羅萬象的把各種你程式當中可能會用得到的內容放在一起,並且給予明確的格式定義,然後在有需要用到的時候,以簡易的選取邏輯指向特定的值,這就是枚舉的用途。

每個變體都代表這個枚舉可能的其中一種具體情況,你可以把它們想像成問卷中的選項、交通號誌的不同顏色、或是遊戲角色可能的動作。這些變體讓程式能夠清楚地表達每一種狀態或結果。

在日常的開發中,枚舉的使用情境非常廣泛。假設你在開發一款遊戲,玩家可以向上、向下、向左或向右移動。如果你只是使用字串或數字來表示方向,很容易混淆,或是在後續維護時遇到錯誤。使用枚舉後,這些方向有了明確的名稱,且如果有未處理的情況,Rust 會在編譯時期提醒你,避免漏掉任何狀態,下面是一個使用範例。

enum Direction {
    Up,    // 代表向上
    Down,  // 代表向下
    Left,  // 代表向左
    Right, // 代表向右
}

在這個範例中,Direction 是一個枚舉,它有四種可能的值:UpDownLeftRight。這樣的設計讓我們可以用明確的名稱來表示方向,當我們想要重新定義一個新的變數為枚舉當中其中一項時,只需要透過::將枚舉當中的其中一個變體取出即可。

使用枚舉

由於枚舉包含多項變體,因此我們可以使用 match 語法來根據不同的變體做對應的操作。這樣的模式匹配讓程式更加直觀、易於理解,並且還可以避免忘記處理某些情況。

fn move_player(direction: Direction) {
    // 使用 match 來處理不同的方向
    match direction {
        Direction::Up => println!("向上移動"),   // 當選擇 Up 時,執行這裡
        Direction::Down => println!("向下移動"), // 當選擇 Down 時,執行這裡
        Direction::Left => println!("向左移動"), // 當選擇 Left 時,執行這裡
        Direction::Right => println!("向右移動"), // 當選擇 Right 時,執行這裡
    }
}

fn main() {
    let player_direction = Direction::Up; // 設定玩家的方向為 Up
    move_player(player_direction); // 呼叫函數來執行對應的動作
}

在這段程式碼中,我們使用 match 語法來檢查 direction 的變體,並執行相應的操作。這樣的寫法比起用數字或字串更具可讀性,而且可以避免處理不完全的錯誤。在move_player的函數當中,我們也針對了不同枚舉的變體下所對應的動作,只要使用以下的方式

move_player(Direction::Up);
move_player(Direction::Down);
move_player(Direction::Left);
move_player(Direction::Right);

就可以完成 上 下 左 右 的移動,當然這只是簡單的範例,讓我們理解枚舉跟函數的基礎搭配用法,請注意,這裡的變體都還未被定義為特定的資料類別,或者是尚未攜帶任何參數,接下來我們要進一步到枚舉的入門用法了。


二、帶參數的枚舉

Rust 的枚舉不僅能表示簡單的狀態或選項,還能攜帶不同類型的數據。這讓枚舉在處理複雜場景時變得非常靈活,能夠在一個類別中管理多種相關資訊。

enum Message {
    Quit,                         // 不帶參數,單純表示退出的訊息
    Move { x: i32, y: i32 },      // 帶有 x 和 y 座標的結構,表示移動
    Write(String),                // 帶有字串參數,表示寫入的訊息
    ChangeColor(i32, i32, i32),   // 帶有 RGB 三個整數參數,表示改變顏色
}

在這裡,我們定義了 Message 枚舉,其中包含了多種不同的訊息類型,下面我們詳細解釋。

  1. Quit

    • 類型:單純的變體,不攜帶數據,也不代表Quit字串類別,就是一個被名為Quit的變體。
    • 用法:用來表示某一狀態或操作,例如退出。
    • 例子Message::Quit 用來告知程式進行退出的動作,不需要傳遞其他資訊。
  2. Move { x: i32, y: i32 }

    • 類型Move 是一個帶有 xy 座標的變體,用來表示移動的動作。
    • 用法:表示移動操作,並且需要提供座標數據。
  3. Write(String)

    • 類型:帶參數的變體,攜帶一個字串 String
    • 用法:表示寫入操作,需要傳遞具體的訊息字串。
  4. ChangeColor(i32, i32, i32)

    • 類型:帶參數的變體,攜帶三個整數,用來表示 RGB 顏色值。
    • 用法:用於改變顏色,需提供具體的 RGB 數值。

使用帶參數的枚舉

當枚舉的變體帶有參數時,我們首先一樣透過::從枚舉當中指定變體,然後接下來再依據參數格式帶入參數即可,透過上述的函數操作範例,我們可以在 match 語法中判斷進入函數的是哪一種枚舉狀態,並且進一步提取這些參數,然後根據具體的情況進行操作。

fn process_message(msg: Message) {
    match msg {
        Message::Quit => println!("退出"), // 當訊息是 Quit 時,顯示"退出"
        Message::Move { x, y } => println!("移動到座標:({}, {})", x, y), // 提取 x 和 y
        Message::Write(text) => println!("寫入訊息:{}", text), // 提取並顯示字串訊息
        Message::ChangeColor(r, g, b) => println!("更改顏色為 RGB: ({}, {}, {})", r, g, b), // 提取 RGB 顏色值
    }
}

fn main() {
    let msg = Message::Move { x: 10, y: 20 }; // 設定移動訊息,帶有 x 和 y 座標
    process_message(msg); // 處理這個訊息

    let text_msg = Message::Write(String::from("你好,Rust!")); // 設定寫入訊息
    process_message(text_msg); // 處理這個訊息
}

可以看出,我們從Message枚舉中選取了Move變體,並在該變體後加入{x: 10, y: 20}帶入作為參數,再將這個變體狀態帶入函數process_message()當中,當然如果想要簡化,這與process_message(Message::Move { x: 10, y: 20 });代表相同的意思,從這行程式碼的命名,就可以將其翻譯成,執行動作:移動到x座標為10,y座標為20的位置,這就是透過枚舉使程式可讀性與編寫效率增加的方法。


三、標準枚舉:Option 類別

以下介紹兩個已經被 Rust 內定好的枚舉,它們常被用於作為判別 "有或沒有" "正確或錯誤" 時的返回值或者回應方式的工具。

Option 是 Rust 中非常常見的枚舉,用於表示可能有值或沒有值的情況,類似於其他語言中的 nullNone。但與它們不同的是,Option 更加安全且易於管理,因為使用Option時需定義清楚 有 與 沒有 的情況分別為何,以避免出現意外情形。

Option 類別

Option<T> 是一個泛型枚舉,裡面包含兩個變體:

  • Some(T):表示有值,T 是具體的類型。
  • None:表示沒有值。
fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None  // 如果分母是 0,返回 None
    } else {
        Some(a / b)  // 否則返回除法結果,包在 Some 裡面
    }
}

fn main() {
    let result = divide(10.0, 2.0); // 正常情況,有值
    match result {
        Some(val) => println!("結果是:{}", val), // 當有值時,顯示結果
        None => println!("無法計算(分母不能為 0)"), // 當沒有值時,顯示錯誤訊息
    }
}

使用 Option 可以避免程式中發生空值錯誤,這讓程式更穩定且易於控制。


四、錯誤處理:Result 類別

Result 是 Rust 中另一個常用的枚舉,主要用來處理操作的成功或失敗,並且可以帶有詳細的錯誤訊息。

Result 類別

Result<T, E> 是一個泛型枚舉,它包含兩個變體:

  • Ok(T):表示操作成功,並帶有成功的結果。
  • Err(E):表示操作失敗,並帶有錯誤的訊息。
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("分母不能為 0")) // 如果分母是 0,返回錯誤訊息
    } else {
        Ok(a / b) // 否則返回成功結果
    }
}

fn main() {
    let result = divide(10.0, 2.0); // 測試正常情況
    match result {
        Ok(val) => println!("結果是:{}", val), // 成功時顯示結果
        Err(err) => println!("錯誤:{}", err), // 錯誤時顯示錯誤訊息
    }
}

這種處理方式讓程式可以明確區分成功和失敗,比起傳統的錯誤處理方法更為安全和可控。

Option 與 Result 比較

特性 Option Result
變體 Some(T) / None Ok(T) / Err(E)
用途 處理可選值或不存在的情況 錯誤處理與結果返回
使用場景 可選數據、默認值 操作成功或失敗的返回

五、Python 中的枚舉

雖然Python 也有枚舉(enum),可以用來表示一組具名的常量。但與 Rust 不同,Python 的枚舉無法攜帶數據,功能上較為有限。

from enum import Enum

class Direction(Enum):
    UP = 1
    DOWN = 2
    LEFT = 3
    RIGHT = 4

def move_player(direction):
    # 使用 if...elif 處理不同的方向
    if direction == Direction.UP:
        print("向上移動")
    elif direction == Direction.DOWN:
        print("向下移動")
    elif direction == Direction.LEFT:
        print("向左移動")
    elif direction == Direction.RIGHT:
        print("向右移動")

move_player(Direction.UP) # 移動到上方

比較:Rust 與 Python 的枚舉

特性 Rust 枚舉 Python 枚舉
帶參數能力 支援 不支援
使用場景 狀態、錯誤、複雜數據 常量定義

在上述範例中,我們根據不同的訊息類型做出對應的操作。這種設計讓程式能夠靈活處理多種情況,而不需要額外定義許多不同的函數,可以定義一組枚舉包含多個變體,每個變體又可再定義攜帶的參數,接著再定義一個函數以對每個變體進行相對應的回應動作,就可以完成多種變化動作讓程式去執行。除此之外,簡單的枚舉也可以用於二元分類用途,目的也是簡化程式對於布林值或者例外情況的判斷。這種語法對於Python開發者而言,算也是開啟了另一項技能,所以相當值得深入探索。

六、總結

最後再補充一個關於枚舉的變體如何攜帶各種類別資料的範例表格

以下是關於「枚舉中的變體如何攜帶各種類型的參數」的示例表格,這樣可以幫助你理解如何使用枚舉的變體來攜帶不同類型的資料。

枚舉變體攜帶不同類型參數的示例

枚舉名稱 變體名稱及參數 資料類型 說明 使用範例
DataTypeExample Integer(i32) 整數 攜帶一個整數值,適用於表示數字資料 DataTypeExample::Integer(42)
Float(f64) 浮點數 攜帶一個浮點數值,用於處理需要小數的數據 DataTypeExample::Float(3.14)
Text(String) 字串 攜帶一段文字資料,適合用於顯示或存儲字元 DataTypeExample::Text(String::from("Hello"))
Boolean(bool) 布林值 攜帶布林值,用於表示 truefalse 的狀態 DataTypeExample::Boolean(true)
Coordinates { x: i32, y: i32 } 結構體 攜帶兩個座標值的結構體,適合用於位置或點的表示 DataTypeExample::Coordinates { x: 10, y: 20 }
Color(u8, u8, u8) 三個 u8 整數 攜帶 RGB 色彩的三個值,用於顏色表示 DataTypeExample::Color(255, 0, 0)
OptionalValue(Option<i32>) 可選值 攜帶一個可能有值或沒有值的整數,用於安全地處理可能缺少的資料 DataTypeExample::OptionalValue(Some(100))
ResultValue(Result<f64, String>) 結果型 攜帶計算結果或錯誤訊息的變體,用於處理計算失敗的情況 DataTypeExample::ResultValue(Ok(3.5))
ComplexData(Vec<i32>, HashMap<String, i32>) 向量和哈希映射 攜帶複雜的資料結構,如向量和哈希映射,用於多重資料存儲 DataTypeExample::ComplexData(vec![1, 2, 3], HashMap::new())

最後,Rust 的枚舉提供了一種強大且靈活的方式來處理多種狀態和數據,無論是用於錯誤處理、狀態管理,還是表達複雜的數據結構,枚舉都能使代碼變得更安全、清晰且易於維護。透過學習和應用 Rust 中的枚舉,你可以大幅提升程式的結構和可讀性,並有效避免常見的空值和錯誤處理問題。馬上就開始動手試試看吧!


上一篇
[Day 8] 結構體與元組:自定義類別
下一篇
[Day 10] 錯誤處理:探討Rust 的 Result 與 Option
系列文
從 Python 開發者的角度學習 Rust —— 從語法基礎到實戰應用14
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言