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::V4
和 IpAddrKind::V6
都是 IpAddrKind
類別。
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
可定義使用 IpAddrKind
的 function,並用 IpAddrKind::V4
或 IpAddrKind::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::
就能直接使用 Some
與 None
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;
}
沒有留言:
張貼留言