Rust枚举和模式匹配
枚举
枚举(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 来获取并使用这些值。