枚举和模式匹配¶
FIXME
枚举的定义¶
枚举enum¶
枚举(Enumerations)是一种用于定义一个类型的值的有限集合的数据结构。枚举类型在Rust中非常强大,它能够帮助开发者更好地表达代码中的概念。因为枚举值只可能是其中一个成员。
在Rust中,使用enum
关键字来定义枚举类型。例如,下面是一个表示不同颜色的枚举类型的示例:
enum Color {
Red,
Blue,
Green,
}
在上面的示例中,Color
是一个枚举类型,它包含了三个可能的值:Red
、Blue
和Green
。这些值被称为枚举的“成员”(variants)。
枚举类型的成员可以带有关联的数据。这使得枚举类型更加灵活。例如,我们可以修改上面的示例,使Color
枚举的成员具有关联的RGB值:
enum Color {
RGB(u8, u8, u8),
Red,
Blue,
Green,
}
在上面的示例中,RGB
成员带有三个u8
类型的参数,分别表示红、绿、蓝的值。
枚举类型可以像结构体一样使用模式匹配(pattern matching)进行解构。下面是一个使用match
语句对Color
枚举的成员进行匹配的示例:
fn print_color(color: Color) {
match color {
Color::Red => println!("The color is red!"),
Color::Blue => println!("The color is blue!"),
Color::Green => println!("The color is green!"),
Color::RGB(r, g, b) => println!("The color is RGB({},{},{})!", r, g, b),
}
}
上面的代码通过match
语句根据不同的枚举成员执行相应的代码块。
枚举Option¶
Rust中并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>
,而且它定义于标准库中,如下:
enum Option<T> {
None,
Some(T),
}
<T>
语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,<T>
意味着 Option
枚举的 Some
成员可以包含任意类型的数据,同时每一个用于 T
位置的具体类型使得 Option<T>
整体作为不同的类型。这里是一些包含数字类型和字符串类型 Option
值的例子:
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
some_number
的类型是 Option<i32>
。some_char
的类型是 Option<char>
,这(与 some_number
)是一个不同的类型。因为我们在 Some
成员中指定了值,Rust 可以推断其类型。对于 absent_number
,Rust 需要我们指定 Option
整体的类型,因为编译器只通过 None
值无法推断出 Some
成员保存的值的类型。这里我们告诉 Rust 希望 absent_number
是 Option<i32>
类型的。
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
如果运行这些代码,将得到类似这样的错误信息:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
<&'a f32 as Add<f32>>
<&'a f64 as Add<f64>>
<&'a i128 as Add<i128>>
<&'a i16 as Add<i16>>
<&'a i32 as Add<i32>>
<&'a i64 as Add<i64>>
<&'a i8 as Add<i8>>
<&'a isize as Add<isize>>
and 48 others
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` due to previous error
在对 Option<T>
进行运算之前必须将其转换为 T
。通常这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。
match控制流结构¶
match¶
Rust 有一个叫做 match
的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。
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,
}
}
当 match
表达式执行时,它将结果值按顺序与每一个分支的模式相比较。如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支,非常类似一个硬币分类器。可以拥有任意多的分支。
每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 match
表达式的返回值。
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
绑定值的模式¶
匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值的。
作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美国在 25 美分的硬币的一侧为 50 个州的每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的 enum
,通过改变 Quarter
成员来包含一个 State
值,示例 6-4 中完成了这些修改:
#[derive(Debug)] // 这样可以立刻看到州的名称
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如果我们的朋友没有的话,他可以将其加入收藏。
在这些代码的匹配表达式中,我们在匹配 Coin::Quarter
成员的分支的模式中增加了一个叫做 state
的变量。当匹配到 Coin::Quarter
时,变量 state
将会绑定 25 美分硬币所对应州的值。接着在那个分支的代码中使用 state
,如下:
fn value_in_cents(coin: Coin) -> u8 {
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
成员中内部的州的值。
匹配Option< T >¶
比如我们想要编写一个函数,它获取一个 Option<i32>
,如果其中含有一个值,将其加一。如果其中没有值,函数应该返回 None
值,而不尝试执行任何操作。
得益于 match
,编写这个函数非常简单,它将看起来像示例 6-5 中这样:
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);
Option类型是一种用于表示可能存在的值的枚举类型。Option有两个可能的值:Some和None。Some包装了一个具体的值,表示存在某个值,而None表示不存在值。
使用Option类型的一些常见操作:
-
创建Option值:
let some_value: Option<i32> = Some(5); // 使用Some包装具体的值 let none_value: Option<i32> = None; // 使用None表示不存在值
-
解包Option值:
如果你确定Option值一定包含具体的值,可以使用unwrap方法解包。但是如果Option值是None,解包会导致panic。因此,在解包之前最好使用match或者if let进行判断。let some_value: Option<i32> = Some(5); let unwrapped_value: i32 = some_value.unwrap(); // 解包Some值,如果是None会导致panic let none_value: Option<i32> = None; let unwrapped_value: i32 = none_value.unwrap(); // 解包None值会导致panic
-
使用match匹配Option值:
let some_value: Option<i32> = Some(5); match some_value { Some(value) => println!("Value is {}", value), None => println!("No value"), } let none_value: Option<i32> = None; match none_value { Some(value) => println!("Value is {}", value), None => println!("No value"), }
-
使用if let判断Option值:
let some_value: Option<i32> = Some(5); if let Some(value) = some_value { println!("Value is {}", value); } else { println!("No value"); } let none_value: Option<i32> = None; if let Some(value) = none_value { println!("Value is {}", value); } else { println!("No value"); }
匹配Some< T >¶
Some
是一种Option
枚举的变体,它表示一个非空的值。Option
类型用于处理可能不存在的值,它有两个变体:Some
和None
。
在使用Option
类型时,可以使用Some
将一个值包装到Option
中,表示该值存在。例如:
let some_value: Option<i32> = Some(5);
some_value
是一个Option<i32>
类型的变量,通过Some(5)
将值5
包装到Option
中。这表示some_value
是一个非空的值。
然后,可以使用模式匹配或unwrap
方法来获取Some
中的值。例如:
match some_value {
Some(value) => println!("Value is {}", value),
None => println!("Value is None"),
}
在上面的示例中,使用模式匹配来检查some_value
的变体。如果是Some
,则将value
绑定到其中的值,并打印出来。如果是None
,则打印出Value is None
。
让我们更仔细地检查 plus_one
的第一行操作。当调用 plus_one(five)
时,plus_one
函数体中的 x
将会是值 Some(5)
。接着将其与每个分支比较。
None => None,
值 Some(5)
并不匹配模式 None
,所以继续进行下一个分支。
Some(i) => Some(i + 1),
Some(5)
与 Some(i)
匹配吗?当然匹配!它们是相同的成员。i
绑定了 Some
中包含的值,所以 i
的值是 5
。接着匹配分支的代码被执行,所以我们将 i
的值加一并返回一个含有值 6
的新 Some
。
接着考虑下示例 6-5 中 plus_one
的第二个调用,这里 x
是 None
。我们进入 match
并与第一个分支相比较。
None => None,
匹配上了!这里没有值来加一,所以程序结束并返回 => 右侧的值 None
,因为第一个分支就匹配到了,其他的分支将不再比较。
将 match
与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:match
一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。
ref
Some(ref xx)
是一种构造方式,用于创建一个包含引用的Option
枚举的Some
变体。Option
是Rust中的一个枚举类型,用于表示一个可能存在或可能不存在的值。
Some(ref xx)
表示一个存在的值,并且它是通过引用方式传递的。这意味着它不会拥有所引用的值,只是持有了一个对该值的引用。这通常用于避免所有权转移的情况,同时允许你在代码的不同部分共享对同一值的引用。
下面是一个示例,展示如何使用Some(ref xx)
创建一个包含引用的Option
:
fn main() {
let value = 42;
let option_value = Some(&value); // 创建一个包含对value的引用的Some
match option_value {
Some(ref x) => println!("Value: {}", x), // 通过引用获取值
None => println!("No value"),
}
}
在这个示例中,option_value
由Some(ref x)
创建,其中x
是对value
的引用。在match
表达式中,我们可以使用ref x
来获取对value
的引用,并打印出它的值。
需要注意的是,Some(ref xx)
只适用于引用类型,对于拥有所有权的类型,应该使用Some(xx)
,其中xx
是值本身,而不是引用。
匹配是穷尽的¶
match
还有另一方面需要讨论:这些分支必须覆盖了所有的可能性。考虑一下 plus_one
函数的这个版本,它有一个 bug 并不能编译:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
Rust 中的匹配是 穷尽的(exhaustive):必须穷举到最后的可能性来使代码有效。
通配模式和 _ 占位符¶
我们希望对一些特定的值采取特殊操作,而对其他的值采取默认操作。想象我们正在玩一个游戏,如果你掷出骰子的值为 3,角色不会移动,而是会得到一顶新奇的帽子。如果你掷出了 7,你的角色将失去新奇的帽子。对于其他的数值,你的角色会在棋盘上移动相应的格子。这是一个实现了上述逻辑的 match
,骰子的结果是硬编码而不是一个随机值,其他的逻辑部分使用了没有函数体的函数来表示,实现它们超出了本例的范围:
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
我们必须将通配分支放在最后,因为模式是按顺序匹配的。
Rust 还提供了一个模式,当我们不想使用通配模式获取的值时,请使用 _
,这是一个特殊的模式,可以匹配任意值而不绑定到该值。这告诉 Rust 我们不会使用这个值,所以 Rust 也不会警告我们存在未使用的变量。
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
我们可以使用单元值(在“元组类型”一节中提到的空元组)作为 _
分支的代码:
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
在这里,我们明确告诉 Rust 我们不会使用与前面模式不匹配的值,并且这种情况下我们不想运行任何代码。
if let 简洁控制流¶
if let
是一种简洁的语法,用于在匹配某个模式时执行代码。它可以用作替代match
表达式的一种方式,尤其适用于只关心一种模式的情况。
下面是if let
的基本语法:
if let <pattern> = <expression> {
// 匹配成功时执行的代码
} else {
// 匹配失败时执行的代码
}
<pattern>
是要匹配的模式,例如变量、元组、结构体等。<expression>
是要匹配的表达式。
下面是一个例子,演示了如何使用if let
来处理Option
类型:
fn main() {
let some_value: Option<i32> = Some(5);
if let Some(x) = some_value {
println!("Got value: {}", x);
} else {
println!("No value");
}
}
在这个例子中,if let
用于匹配some_value
是否是Some
,如果是,就将x
绑定为some_value
中的值,并执行println!
语句。如果some_value
是None
,则执行else
块中的代码。
使用if let
可以使代码更简洁和易读,特别是当只关心某个模式的情况下。
let v = Some(4);
match v {
Some(3) => println!("three"),
_ => ()
}
if let Some(3) = v {
println!("three");
}
上面代码效果与下面代码效果相同