iT邦幫忙

2024 iThome 鐵人賽

DAY 18
1
Software Development

螃蟹幼幼班:Rust 入門指南系列 第 18

Day18 - 列舉與 match

  • 分享至 

  • xImage
  •  

列舉基礎

在程式設計中,我們經常需要表示一個變數可能有多個固定的選項或狀態。例如,一週中的天數、撲克牌的花色、或者用戶的權限等級。這就是列舉(enumerations,有時簡稱enums)發揮作用的地方。

透過列舉我們可以定義一種自定義類型,該類型可以有一組預定義的可能值。這些可能的值被稱為變體(variants)。
列舉的寫法是用 enum 關鍵字,後面接自定的列舉型別名稱,接著用大括號,裡面可以指定不同變體並且可以包含型別,讓列舉變體可以攜帶不同類型的數據,使其更具靈活性。
列舉和其變體習慣上是用CamelCase命名。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

在上面的例子中,QuitMoveWriteChangeColor都是 Message 的變體,都會被視作是 Message型別,每個變體可以攜帶不同型別的資料。。

列舉的一種主要用法是搭配 match 來執行不同的程式流程。match 可以使用一系列模式來配對數值並依據配對到的模式來執行對應的程式,強大的地方在於模式表達的清楚程度以及編譯器會確保所有可能的情況都被處理到

match 語法

match 能夠用在許多不同的型別上,包括數值、字串、元組、結構體,甚至是引用,提供了一種方便的方式來解構和匹配多種模式。

match 指定型別之後接著一個表達式,所以我們會用一個大括號,裡面會有不同的分支,各分支用 ,分隔,每個分支有兩個部分:一個模式以及對應的程式碼,每個分支的程式碼也是表達式,配對到的分支中表達式的數值結果就會是整個 match 表達式的回傳值。

match的配對方式有順序性,和 if-else一樣,如果前面有符合的分支就不會再去執行後面的分支。如果匹配分支的程式碼很短的話,可以不用到大括號,如果想要在配對分支執行多行程式碼的話,就必須用大括號,大括號內根據情況決定最後一行要不要加分號來回傳數據。

列舉沒有一定要和結構體搭配使用,除了型別檢查外,match也可以結構數值、針對數值做匹配等等。以下舉一個結構體搭配match的用法。

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

fn print_point(point: Point) {
    match point {
        Point { x: 0, y: 0 } => println!("Origin"),
        Point { x, y: 0 } => println!("On the x-axis at ({}, 0)", x),
        Point { x: 0, y } => println!("On the y-axis at (0, {})", y),
        Point { x, y } => println!("At coordinates ({}, {})", x, y),
    }
}

fn main() {
    let p = Point { x: 0, y: 0 };
    print_point(p);
}

以上的例子可以看到針對 xy是不是 0 不同的匹配條件,

接著我們看一下之前在結構體介紹舉的例子,以下的寫法有些缺點,包含不同的 Access 彼此沒有關聯性、 perform_admin_action 只有處理其中一種型別,如果我要處理另外一種 Access 的話還要另外寫一個函數,外部在使用這些函數還要用 if-else,如果再多一種 Access 甚至有可能漏處理型別。

struct AdminAccess;
struct GuestAccess;

fn main() {
    let admin = AdminAccess;
    let guest = GuestAccess;
    perform_admin_action(admin);
}

fn perform_admin_action(_admin: AdminAccess) {
    println!("Admin action performed successfully!");
}

列舉搭配 match

我們把它重新寫一下,用結構體定義一個UserAdminAccessGuestAccess 用列舉包裝起來,變體會位於列舉命名空間底下,所以用 :: 來連接列舉中的變體,並建立不同的 Access 變體實例,這樣的好處是他們現在都是屬於 Access 的型別,所以我們就可以定義一個函數的參數是 Access 型別再搭配 match 來處理。

enum Access {
    Admin,
    Guest,
}

struct User {
    name: String,
    access: Access,
}

impl User {
    fn create(name: String, access: Access) -> Self {
        User {
            name,
            access,
        }
    }
}

fn perform_admin_action(access: Access) {
    match access {
        Access::Admin => println!("Admin action performed successfully!")
    }
}

fn main() {
    let admin = User::create(String::from("Alice"), Access::Admin);
    let guest = User::create(String::from("Bob"), Access::Guest);

    perform_admin_action(admin.access);
    perform_admin_action(guest.access);
}

如果有漏掉處理列舉變體,編譯器還會直接報錯,這是match的徹底檢查(exhaustiveness checking)的機制,確保所有情況都被處理了。

error[E0004]: non-exhaustive patterns: `Access::Guest` not covered
  --> src/main.rs:21:11
   |
21 |     match access {
   |           ^^^^^^ pattern `Access::Guest` not covered
   |
note: `Access` defined here
  --> src/main.rs:1:6
   |
1  | enum Access {
   |      ^^^^^^
2  |     Admin,
3  |     Guest,
   |     ----- not covered
   = note: the matched value is of type `Access`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
   |
22 ~         Access::Admin => println!("Admin action performed successfully!"),
23 +         Access::Guest => todo!()
   |

我們再補上漏掉處理的變體,就可以正常執行了。

fn perform_admin_action(access: Access) {
    match access {
        Access::Admin => println!("Admin action performed successfully!"),
        Access::Guest => println!("You do not have admin privileges."),
    }
}

match 的預設值

或是,我們再多一種變體 Member ,同時把 perform_admin_action 改寫成 User 的方法,那現在就需要在 match access 裡面再多一行 Access::Member => println!("You do not have admin privileges."), 嗎?其實可以不用, match 有一個叫做 Catch-all 模式,簡單說就是預設模式,所有沒有列在前面的變體都會落在這個邏輯裡面,而且一定要放在 match 最後一行。可以用一個變數名稱把變體當參數傳進這個模式,這個變數名稱可以自行決定,以下用 other

#[derive(Debug)]
enum Access {
    Admin,
    Member,
    Guest,
}

struct User {
    name: String,
    access: Access,
}

impl User {
    fn create(name: String, access: Access) -> Self {
        User {
            name,
            access,
        }
    }

    fn perform_admin_action(&self) {
        match &self.access {
            Access::Admin => println!("Admin action performed successfully!"),
            other => println!("You do not have admin privileges. Your access level: {:?}", other)
        }
    }
}

fn main() {
    let admin = User::create(String::from("Alice"), Access::Admin);
    let member = User::create(String::from("Alex"), Access::Member);
    let guest = User::create(String::from("Bob"), Access::Guest);

    admin.perform_admin_action();
    member.perform_admin_action();
    guest.perform_admin_action();
}

又或是模式裡面不需要判斷變體,可以直接用佔位符_代表任何情況。

    fn perform_admin_action(&self) {
        match &self.access {
            Access::Admin => println!("Admin action performed successfully!"),
            _ => println!("You do not have admin privileges.")
        }
    }

如果不想要執行任何動作的話,甚至可以直接把後面的動作直接改成 () 單元數值。

    fn perform_admin_action(&self) {
        match &self.access {
            Access::Admin => println!("Admin action performed successfully!"),
            _ => ()
        }
    }

這樣就會顯式地告訴 Rust 除了先訂出來的模式,其他沒有被配對到的情況我們都不會用到它的數值,也不想在此執行任何動作。

結語

總結來說,enum 和 match 是 Rust 中強大且靈活的工具。
enum 讓我們能夠清楚地表達多個可能的狀態,而 match 則提供了一個直觀且強型別的模式匹配機制,既能靈活的根據不同模式有不同的行為,也能透過Catch-all 模式來處理所有未列出的情況,讓程式碼既能夠保持簡潔,又能處理複雜的邏輯,同時 Rust 編譯器的嚴格性也確保了 match 的每一個分支都被正確處理,讓我們能兼顧安全性和性能來處理各種情況。
這些語法特性不僅提高了程式碼的可讀性,還會在編譯時就檢查及發現發現邏輯錯誤,減少了執行時的風險。
除此之外 Rust 編譯器會對match表達式進行優化,使其有機會比等效的if-else更有效率。


上一篇
Day17 - 方法與關聯函數
下一篇
Day19 - 更多的列舉
系列文
螃蟹幼幼班:Rust 入門指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言