枚舉
枚舉(enumerations),也被稱作 enums。枚舉允許你通過列舉可能的 成員(variants) 來定義一個類型。讓我們看看一個需要訴諸於代碼的場景,來考慮為何此時使用枚舉更為合適且實用。假設我們要處理 IP 地址。目前被廣泛使用的兩個主要 IP 標准:IPv4(version four)和IPv6(version six)。這是我們的程序可能會遇到的所有可能的 IP 地址類型:所以可以 枚舉出所有可能的值,這也正是此枚舉名字的由來。任何一個 IP 地址要么是 IPv4 的要么是 IPv6 的,而且不能兩者都是。IP 地址的這個特性使得枚舉數據結構非常適合這個場景,因為枚舉值只可能是其中一個成員。IPv4 和 IPv6 從根本上講仍是 IP 地址,所以當代碼在處理適用於任何類型的 IP 地址的場景時應該把它們當作相同的類型。
可以通過在代碼中定義一個 IpAddrKind 枚舉來表現這個概念並列出可能的 IP 地址類型, V4 和 V6 。這被稱為枚舉的 成員(variants):
enum IpAddrKind { // 現在 IpAddrKind 就是一個可以在代碼中使用的自定義數據類型了。 V4, V6, }
枚舉值
正如上我們創建了一個IpAddKind類型,它現在就是一個自定義數據類型了,我們可以使用它來創建不同的實例、或者定義一個函數來獲取所有類似的IpAddKind類型的值:
#[derive(Debug)] enum IpAddrKind { // 現在 IpAddrKind 就是一個可以在代碼中使用的自定義數據類型了。 V4, V6, } fn main() { // 創建兩個IpAddrKind實例 let four = IpAddrKind::V4; let six = IpAddrKind::V6; println!("{:?},{:?}", four, six); // V4,V6 fn route(ip_type: IpAddrKind) { println!("{:?}", ip_type); } route(six); // V6 }
將枚舉和元組配合
#[derive(Debug)] enum IpAddrKind { // 現在 IpAddrKind 就是一個可以在代碼中使用的自定義數據類型了。 V4, V6, } struct IpAddr { kind:IpAddrKind, address:String, } fn main() { let home = IpAddr { kind:IpAddrKind::V4, address:String::from("127.0.01"), }; }
上面為了描述ip的不同類型創建了兩個結構體,第一個kind用來表示ip類型,與之關聯的是address字段是具體的值,所以不是很方便,我們可以將數據直接放入到枚舉中來簡化
#[derive(Debug)] enum IpAddr { // 將數據附加到枚舉的每個成員上,這樣就不需要一個額外的結構體 V4(String), V6(String), } enum IpAddr1 { V4(u8, u8, u8, u8), V6(String), } fn main() { let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::1")); println!("{:?}", loopback); let home1 = IpAddr1::V4(127,0,0,1); // 可以將任意類型的數據放入枚舉成員中:例如字符串、數字類型或者結構 體。甚至可以包含另一個枚舉 }
復雜的枚舉
內嵌了多種多樣的類型:
#[derive(Debug)] enum Message { Quit, // Quit 沒有關聯任何數據。 Move { x: i32, y: i32 }, // 包含一個匿名結構體。 Write(String), // 包含單獨一個 String 。 ChangeColor(i32, i32, i32), // 包含三個 i32 。 } // 上面的枚舉類似於定義了以下幾個結構體 struct QuitMessage; // 類單元結構體 struct MoveMessage { x: i32, y: i32, } struct WriteMessage(String); // 元組結構體 struct ChangeColorMessage(i32, i32, i32); // 元組結構體 // 不過,如果我們使用不同的結構體,由於它們都有不同的類型, // 我們將不能像使用示例 6-2 中定義的 Message 枚舉那樣,輕易的定義一個 // 能夠處理這些不同類型的結構體的函數,因為 枚舉是單獨一個類型
也就是說,枚舉中可以達到定義結構體的效果,同時枚舉類型可以包含多個類似結構體的數據,所以可以處理不同類型的類型數據,但上層是統一的枚舉類型,結構體和枚舉還有另一個相似點:就像可以使用 impl 來為結構體定義方法那樣,也可以在枚舉上定義方法。這是一個定義於我們 Message 枚舉上的叫做 call 的方法:
#[derive(Debug)] enum Message { Quit, // Quit 沒有關聯任何數據。 Move { x: i32, y: i32 }, // 包含一個匿名結構體。 Write(String), // 包含單獨一個 String 。 ChangeColor(i32, i32, i32), // 包含三個 i32 。 } impl Message { fn call(&self) { println!("OK") } } fn main() { let m = Message::Write(String::from("hello")); m.call(); }
Option枚舉和其相對於空值的優勢
Option 是標准庫定義的另一個枚舉。 Option 類型應用廣泛因為它編碼了一個非常普遍的場景,即一個值要么有值要么沒值。從類型系統的角度來表達這個概念就意味着編譯器需要檢查是否處理了所有應該處理的情況,這樣就可以避免在其他編程語言中非常常見的 bug。
空值
enum Option<T> { Some(T), None, }
Option<T> 枚舉是如此有用以至於它甚至被包含在了 prelude 之中,你不需要將其顯式引入作用域。另外,它的成員也是如此,可以不需要 Option:: 前綴來直接使用 Some 和None 。即便如此 Option<T> 也仍是常規的枚舉, Some(T) 和 None 仍是 Option<T> 的成員。
fn func(y: Option<i8>) -> i8 { match y { Option::Some(i8) => 5, Option::None => 0, } } fn main() { let some_num = Some(5); let some_string = Some("a string"); let absent_num: Option<i32> = None; // 如果使用 None 而不是 Some ,需要告訴 Rust Option<T> 是什么類型的, // 因為編譯器只通過 None 值無法推斷出 Some 成員保存的值的類型。 println!("{:?}",some_string); let x: i8 = 5; let y: Option<i8> = Some(5); // println!("{}",y+x); // 報錯 因為i8和Option<i8> 類型不同,原型是Option中還有None,必須要多一層判斷是不是None let y1 = func(y); // 使用match進行判斷 println!("{}", x + y1); // 10 }
總的來說,為了使用 Option<T> 值,需要編寫處理每個成員的代碼。你想要一些代碼只當擁有 Some(T) 值時運行,允許這些代碼使用其中的 T 。也希望一些代碼在值為 None 時運行,這些代碼並沒有一個可用的 T 值。 match 表達式就是這么一個處理枚舉的控制流結構:它會根據枚舉的成員運行不同的代碼,這些代碼可以使用匹配到的值中的數據。
match 控制流運算符
Rust 有一個叫做 match 的極為強大的控制流運算符,它允許我們將一個值與一系列的模式相比較,並根據相匹配的模式執行相應代碼。模式可由字面值、變量、通配符和許多其他內
#[derive(Debug)] enum Coin { Penny, Nickel, Dime, Quarter, } fn main() { fn value_in_cents(coin: Coin) -> i8 { match coin { Coin::Penny => 1, Coin::Nickel => { // 可以是一個多語句的表達式 println!("Luck Boy"); 5 } Coin::Dime => 10, Coin::Quarter => 25, } } let yijiao = Coin::Penny; let wumao = Coin::Nickel; println!("{:?},{:?}", value_in_cents(yijiao), value_in_cents(wumao)); }
#[derive(Debug)] enum usStatus { Alabama, Alaska, } enum Coin { Penny, Nickel, Dime, Quarter(usStatus), // 枚舉中包含另一個枚舉,說明這個數據的數據類型來自於另一個枚舉 } fn main() { fn value_in_cents(coin: Coin) -> i8 { match coin { Coin::Penny => 1, Coin::Nickel => { // 可以是一個多語句的表達式 println!("Luck Boy"); 5 } Coin::Dime => 10, Coin::Quarter(xxx) => { println!("State quarter from {:?}!", xxx); // State quarter from Alabama! 25 } } } let wumao = Coin::Quarter(usStatus::Alabama); println!("{:?}", value_in_cents(wumao)); }
如果調用 value_in_cents(Coin::Quarter(UsState::Alaska)) , coin 將是Coin::Quarter(UsState::Alaska) 。當將值與每個分支相比較時,沒有分支會匹配,直到遇到Coin::Quarter(state) 。這時, state 綁定的將會是值 UsState::Alaska 。接着就可以在println! 表達式中使用這個綁定了,像這樣就可以獲取 Coin 枚舉的 Quarter 成員中內部的州的值。
匹配Option<T>
我們在之前的部分中使用 Option<T> 時,是為了從 Some 中取出其內部的 T 值;我們還可以像處理 Coin 枚舉那樣使用 match 處理 Option<T> !也就是說我們可以通過T來判斷傳入的變量是否是該類型的,並返回:
#[derive(Debug)] fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), } } fn main() { let five = Some(5); let six = plus_one(five); let none = plus_one(None); println!("{:?},{:?},{:?}", five, six, none); // Some(5),Some(6),None }
plus_one 函數體中的 x 將會是值 Some(5) 。接着將其與每個分支比較, 值 Some(5) 並不匹配模式 None ,所以繼續進行下一個分支。Some(5) 與 Some(i) 匹配嗎?當然匹配!它們是相同的成員。 i 綁定了 Some 中包含的值,所以 i 的值是 5 。接着匹配分支的代碼被執行,所以我們將 i 的值加一並返回一個含有值 6 的新 Some 。這里 x 是 None 。我們進入 match 並與第一個分支相比較,所以直接返回None。 注意:我們沒有處理 None 的情況,所以這些代碼會造成一個 bug。
_ 通配符
Rust 也提供了一個模式用於不想列舉出所有可能值的場景。例如, u8 可以擁有 0 到 255 的有效的值,如果我們只關心 1、3、5 和 7 這幾個值,就並不想必須列出 0、2、4、6、8、9,一直到 255 的值。所幸我們不必這么做:可以使用特殊的模式 _ 替代:
fn main() { let some_u8 = 0u8; match some_u8 { 1 => println!("one"), 5 => println!("five"), _ => () // _ 模式會匹配所有的值, () 就是 unit 值,所以 _ 的情況什么也不會發生 } // 然而, match 在只關心 一個 情況的場景中可能就有點啰嗦了。為此 Rust 提供了 if let 。 }
if let 簡單控制流
if let 語法讓我們以一種不那么冗長的方式結合 if 和 let ,來處理只匹配一個模式的值而忽略其他模式的情況,因為其他匹配的次數太多了,如上例:
fn main() { // 復雜 let some_u8 = Some(8); match some_u8 { Some(3) => println!("three"), _ => (), } // 簡單 if let Some(8) = some_u8 { println!("eight"); } }
可以認為 if let 是 match 的一個語法糖,它當值匹配某一模式時執行代碼而忽略所有其他值。結合if-let我們可以優化上上個代碼:
#[derive(Debug)] enum usStatus { Alabama, Alaska, } enum Coin { Penny, Nickel, Dime, Quarter(usStatus), // 枚舉中包含另一個枚舉,說明這個數據的數據類型來自於另一個枚舉 } fn main() { let coin = Coin::Penny; let mut count = 0; // match coin { // Coin::Quarter(xxx) => println!("State quarter from {:?}", xxx), // _ => count += 1, // } // println!("first:{}", count); // 1 if let Coin::Quarter(xx) = coin { println!("State quarter from {:?}", xx); } else { count += 1; } println!("second:{}", count); // 1 }
標准庫的 Option<T> 類型是如何幫助你利用類型系統來避免出錯的。當枚舉值包含數據時,你可以根據需要處理多少情況來選擇使用 match 或 if let 來獲取並使用這些值。