2020年3月23日

rust06 enums and pattern matching

enumerations 也稱為 enums,可列舉所有可能的值,定義一個新的類別。首先會了解如何定義 enums,以及資料編碼的方法。然後會了解一個特殊的 enum,就是 Option,代表不是某個值,就是另一個值。然後介紹 pattern match expression,最後是 if let,一個方便處理 enum 的方法。

rust 的 enum 類似 F#, OCaml, Haskell 的 algebraic data types

Defining an enum

假設要處理 IPv4, IPv6 兩種 IP Address,程式會遇到兩種 IP,故要用 enum 列出所有可能的值。

任何一個 IP,不是 IPv4 就是 IPv6,不能兩個都是,但 IPv4, IPv6 都是 IP Address。

enum IpAddrKind {
    V4,
    V6,
}

enum values

產生兩個不同成員的 instances,IpAddrKind::V4IpAddrKind::V6 都是 IpAddrKind 類別。

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;   

可定義使用 IpAddrKind 的 function,並用 IpAddrKind::V4IpAddrKind::V6 呼叫該 function

fn route(ip_type: IpAddrKind) { }

route(IpAddrKind::V4);
route(IpAddrKind::V6);

另外用 struct 儲存實際的 IP Address

enum IpAddrKind {
    V4,
    V6,
}

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};

可以用另一種更簡潔的方式,表達相同的概念,就是直接將資料放入 enum,這樣就不需要 struct。

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

用 enum 代替 struct 還有其他優點:每個成員可以處理不同類與數量的資料。

例如將 IPv4 分成四個 0~255 的數字

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

標準函式庫提供了 IPv4, IPv6 的定義:

struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

可以將任意資料放入 enum 成員中,例如 String, 數字, structs,放入另一個 enum 也可以。

另一個範例,裡面有不同類別的值

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

這跟用多個不同的 struct 定義很類似,但 struct 會是不同的類別,不能像 enum message 一樣,可定義一個同時能處理這些類別的 function

struct QuitMessage; // 類單元結構體
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // 元組結構體
struct ChangeColorMessage(i32, i32, i32); // 元組結構體

enum 也可以用 impl 定義 method,這個範例是定義 call 這個 method

impl Message {
    fn call(&self) {
        // 在這裡定義方法body
    }
}

let m = Message::Write(String::from("hello"));
m.call();

The Option Enum and Its Advantages Over Null Values

Option 是標準函式庫定義的另一個 enum。代表有值或是沒有值。這也就是說,可以處理掉 Null 造成的問題。

rust 並沒有其他語言都有的 null value,在有 null 的程式語言中,變數通常是 null 或是 non-null

null 的發明者 Tony Hoare 在 2009年"Null References: The Billion Dollar Mistake" 演講裡面說,null 是一個 billion-dollar mistake,原本他是要設計一個可透過 compiler 自動檢查,保證所有引用的使用都是安全的。但他另外增加了 null reference,因為他很容易實作出來。但這是一個很容易發生的 error, vaulerabilities, system crash,也就是這四十多年造成了數十億美元的損失。

使用 null value 會造成 error,非常容易出現。

但 null 本身還是有意義的,代表目前無效或是不存在的值。

rust 沒有 null,但有提供 Option<T>,包含在 prelude 中,也不需要 Option:: 就能直接使用 SomeNone

enum Option<T> {
    Some(T),
    None,
}

宣告時,如果使用 None,必須明確定義類別,因為 compiler 不知道怎麼判斷類別。

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

Option<T> 比 null 好的原因是資料類別檢查,另外不需要擔心變數是不是空值,只要不是 Option<T> 就不可能是空值。

例如無法將 Option<i8>i8 相加

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

可閱讀 Enum std::option::Option 文件,了解要如何使用 Option

例如用 match 處理 Some 與 None 兩種狀況

    match self {
        Option::Some(val) => val,
        Option::None =>
            panic!("called `Option::unwrap()` on a `None` value"),
    }

The match Control Flow Operator

match 可將 value 與一系列模式相比較匹配,模式可由 literal, 變數, wildcards 或其他內容組成。

match 可想像為硬幣分類器。valueincents 可取得硬幣的價值。

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        },
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

match 很像 if,但 if expression 要回傳 bool,而 match 可回傳任意類別。

match 會依照順序進行比較,會執行匹配的第一個值,回傳該值後面的 expression 回傳值。也可以用 {} 加上多行程式碼。

Patterns that Bind to Values

match 的另一個功能是綁定匹配模式的部分值,也就是從 enum 成員中提取值。

ex: 1999~2008,US 在每個州印刷了不同的 25 美分,但其他硬幣就沒有這種設計,將 State 這個資訊加入 enum 改變 Quarter。

Quarter 成員存放了一個 UsState 值的 Coin enum,匹配到 Coin::Quarter 時,state 將會綁定 25 美分硬幣所對應州的值。接著在那個分支的代碼中使用 state

#[derive(Debug)] // 這樣可以可以立刻看到州的名稱
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        },
    }
}

呼叫 value_in_cents(Coin::Quarter(UsState::Alaska))時,coin 將是 Coin::Quarter(UsState::Alaska)。當將值與每個分支相比較時,直到遇到 Coin::Quarter(state)。這時,state 綁定的將會是值 UsState::Alaska。接著就可以在 println! 表達式中使用這個綁定了,像這樣就可以獲取 Coin 枚舉的 Quarter 成員中內部的州的值。

Matching with Option<T>

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

Matches Are Exhaustive

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

剛剛的 plus_one 如果忘了匹配 None,就會發生編譯錯誤

error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:2:11
  |
2 |     match x {
  |           ^ pattern `None` not covered

rust 的 matching 是 exhaustive 的,必須要窮舉,將所有可能性都列出來。

The _ Placeholder

如果真的不想列出所有可匹配的條件,可以用 _替代。

最後的 _ 會匹配所有上面沒有的值

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

但這樣在只關心一種條件的狀況,語法就太多。因此 rust 提供了 if let

Concise Control Flow with if let

let some_u8_value = Some(0u8);
match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}

可以用 if let 替代,程式碼會比較少,但會失去 match 的 exhaustive 窮舉特性

if let Some(3) = some_u8_value {
    println!("three");
}

可以在 if let 再加上 else

let mut count = 0;
match coin {
    Coin::Quarter(state) => println!("State quarter from {:?}!", state),
    _ => count += 1,
}

上面的程式碼可以改寫為

let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}

References

The Rust Programming Language

中文版

中文版 2

沒有留言:

張貼留言