1、枚举的定义
枚举(enumerations),也被称作 enums。枚举允许你通过列举可能的 成员(variants)来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做 Option
,它代表一个值要么是某个值要么什么都不是。然后会讲到在 match
表达式中用模式匹配,针对不同的枚举值编写相应要执行的代码。最后会介绍 if let
,另一个简洁方便处理代码中枚举的结构。
下面看下下面这个示例:
#[derive(Debug)] enum Sex { Man, Woman, } fn main() { let var = Sex::Man; println!("value is {:?}", var) }
从上面代码示例中,我们把性别可以枚举出来,引用枚举类型的某一个值的时候,可以通过枚举名后面加一堆冒号来引用枚举中的某一个属性值。
下面这个例子,我们可以在枚举中,它的成员可以有多种类型:
enum op { name(String), time(i32), People { name: String, age: i32 }, }
有关联值的枚举的方式和定义多个不同类型的结构体的方式很相像,除了枚举不使用 struct
关键字以及其所有成员都被组合在一起。
结构体和枚举还有另一个相似点:就像可以使用
impl
来为结构体定义方法那样,也可以在枚举上定义方法。
enum Op { Name(String), Time(i32), People { name: String, age: i32 }, } impl Op { fn say(&self) {} }
让我们看看标准库中的另一个非常常见且实用的枚举:Option
。
1.1 Option 枚举和其相对于空值的优势
这一部分会分析一个 Option
的案例,Option
是标准库定义的另一个枚举。Option
类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。
例如,如果请求一个非空列表的第一项,会得到一个值,如果请求一个空的列表,就什么也不会得到。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。
编程语言的设计经常要考虑包含哪些功能,但考虑排除哪些功能也很重要。Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。
问题不在于概念而在于具体的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>
,而且它定义于标准库中,如下:
fn main() { enum Option<T> { None, Some(T), } }
Option<T>
也仍是常规的枚举,Some(T)
和 None
仍是 Option<T>
的成员。<T>
语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,所以你需要知道的就是 <T>
意味着 Option
枚举的 Some
成员可以包含任意类型的数据,同时每一个用于 T
位置的具体类型使得 Option<T>
整体作为不同的类型。这里是一些包含数字类型和字符串类型 Option
值的例子:
enum Option<T> { None, Some(T), } let some_number = Some(5000); let some_char = Some('e'); let some_boolean = Some(true);
让我们再看一下如下示例,定义如下2个值进行相加会怎么样?
fn main() { enum Option<T> { None, Some(T), } let some_number: i8 = 5; let absent_number: Option<i8> = Some(5); let plus = some_number + absent_number; }
运行结果如下所示:
在这里有2个严重的问题:
第一个问题是let absent_number: Option<i8> = Some(5); 在这里赋值的时候会报错,这2个类型名看起来很像,但实际上是不同的类型,无法进行赋值操作。
第二个是不同类型进行相加的时候,当在 Rust 中拥有一个像 i8
这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。只有当使用 Option<i8>
(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。
2、match 控制流结构
Rust 有一个叫做 match
的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成;
我们看一下如下示例,能够更清楚的明白match的作用:
fn main() { enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } let res = value_in_cents(Coin::Nickel); print!("result {}", res) // result 5 }
match 的作用,其他跟其他语言(例如,JavaScript)中的switch差不多,以上代码中,方法接收了一个枚举类型,match根据枚举类型的不同成员来返回的不同的值,类似不同的分支,符合条件的分支,才会被最后返回,如果匹配到了某一个分支,想在执行其他逻辑的时候,可以加一对花括号,在里面写对应的逻辑即可。
fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { print!("res: 执行到这了"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } }
2.1 匹配 Option<T>
下面编写一个函数,它获取一个 Option<i32>
,如果其中含有一个值,将其加一。如果其中没有值,函数应该返回 None
值。
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); println!("{:?} {:?} {:?}", five, six, none) // Some(5) Some(6) None
2.2 匹配是穷尽的
match
还有另一方面需要讨论:这些分支必须覆盖了所有的可能性。否则不能进行编译。
fn plus_one(x: Option<i32>) -> Option<i32> { match x { Some(i) => Some(i + 1), } }
根据上面错误提示,我们知道Rust中match匹配必须是穷尽的,否则无法编译通过。
2.3 通配模式和 _ 占位符
我们看一下如下示例:
fn main() { let dice_roll = 9; match dice_roll { 3 => 3, 7 => 7, hello => 9, }; }
3和7会匹配对应的值,定义一个变量例如:hello,则可以匹配其他任意情况下的值。
即使我们没有列出 u8
所有可能的值,这段代码依然能够编译,因为最后一个模式将匹配所有未被特殊列出的值。这种通配模式满足了 match
必须被穷尽的要求。请注意,我们必须将通配分支放在最后,因为模式是按顺序匹配的。如果我们在通配分支后添加其他分支,Rust 将会警告我们,因为此后的分支永远不会被匹配到。
Rust 还提供了一个模式,当我们不想使用通配模式获取的值时,请使用_
,这是一个特殊的模式,可以匹配任意值而不绑定到该值。这告诉 Rust 我们不会使用这个值,所以 Rust 也不会警告我们存在未使用的变量。
fn main() { let dice_roll = 9; match dice_roll { 3 => 3, 7 => 7, _ => 9, }; }
当我们匹配到其他情况,这种情况下我们不想运行任何代码。可以返回一个空元组,如下所示:
fn main() { let dice_roll = 9; match dice_roll { 3 => three(), 7 => seven(), _ => (), } fn three() {} fn seven() {} }
3、if let 简洁控制流
我们先看一个示例:
fn main() { let config_max = Some(3u8); match config_max { Some(max) => println!("The maximum is configured to be {}", max), _ => (), } }
如果值是 Some
,我们希望打印出 Some
成员中的值,这个值被绑定到模式中的 max
变量里。对于 None
值我们不希望做任何操作。为了满足 match
表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上 _ => ()
,这样也要增加很多烦人的样板代码。
为了简化代码,可以使用if let 来简化一下:
fn main() { let config_max = Some(3u8); if let Some(max) = config_max { println!("res {}", max) } }
使用 if let
意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去 match
强制要求的穷尽性检查。match
和 if let
之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。
换句话说,可以认为 if let
是 match
的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。
至于下环线匹配的模式,可以通过if let else 来实现,如下所示:
fn main() { let mut count = 0; let config_max = Some(3u8); if let Some(max) = config_max { println!("res {}", max) } else { count += 1; } }