Option枚举与match
Option枚举
Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。空值尝试表达的概念:空值是一个因为某种原因目前无效或缺失的值。
Rust 没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>
,而且它定义于标准库中,如下:
enum Option<T> {
Some(T),
None,
}
Option
是标准库定义的另一个枚举。Option
类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。
<T>
意味着 Option
枚举的 Some
成员可以包含任意类型的数据。这里是一些包含数字类型和字符串类型 Option
值的例子:
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
如果使用 None
而不是 Some
,需要告诉 Rust Option<T>
是什么类型的,因为编译器只通过 None
值无法推断出 Some
成员保存的值的类型。
当有一个 Some
值时,我们就知道存在一个值,而这个值保存在 Some
中。当有个 None
值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。
因为 Option<T>
和 T
(这里 T
可以是任何类型)是不同的类型,编译器不允许像一个肯定有效的值那样使用 Option<T>
。例如,这段代码不能编译,因为它尝试将 Option<i8>
与 i8
相加:
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
如果运行这些代码,将得到类似这样的错误信息:
error[E0277]: cannot add `Option<i8>` to `i8`
--> src\main.rs:11:17
|
11 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `my_project` due to previous error
错误信息意味着 Rust 不知道该如何将 Option<i8>
与 i8
相加,因为它们的类型不同。当在 Rust 中拥有一个像 i8
这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。只有当使用 Option<i8>
(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。
换句话说,在对 Option<T>
进行 T
的运算之前必须将其转换为 T
。通常这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。
只要一个值不是 Option<T>
类型,你就 可以 安全的认定它的值不为空。
match控制流运算符
Rust 有一个叫做 match
的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。模式可由字面量、变量、通配符和许多其他内容构成; (类似switch)
示例:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
print!("Penny!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {
let a = value_in_cents(Coin::Penny);
let b = value_in_cents(Coin::Nickel);
let c = value_in_cents(Coin::Dime);
let d = value_in_cents(Coin::Quarter);
print!("a:{}\n", a);
print!("b:{}\n", b);
print!("c:{}\n", c);
print!("d:{}\n", d);
}
输出:
Penny!
a:1
b:5
c:10
d:25
match
关键字后跟一个表达式,它可以是任何类型.
match
的分支。一个分支有两个部分:一个模式和一些代码。第一个分支的模式是值 Coin::Penny
而之后的 =>
运算符将模式和将要运行的代码分开。这里的代码就仅仅是值 1
。每一个分支之间使用逗号分隔。
当 match
表达式执行时,它将结果值按顺序与每一个分支的模式相比较。如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支。可以拥有任意多的分支。
每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 match
表达式的返回值。
如果分支代码较短的话通常不使用大括号,如果想要在分支中运行多行代码,可以使用大括号。
绑定值的模式
匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值的。
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),//Quarter关联UsState
}
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
}
}
}
fn main() {
let a = value_in_cents(Coin::Penny);
let b = value_in_cents(Coin::Nickel);
let c = value_in_cents(Coin::Dime);
let d = value_in_cents(Coin::Quarter(UsState::Alabama));
let e = value_in_cents(Coin::Quarter(UsState::Alaska));
print!("a:{}\n", a);
print!("b:{}\n", b);
print!("c:{}\n", c);
print!("d:{}\n", d);
print!("e:{}\n", e);
}
输出:
State quarter from Alabama!
State quarter from Alaska!
a:1
b:5
c:10
d:25
e:25
匹配Option
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);
print!("five:{}\n", five.unwrap());
print!("six:{}\n", six.unwrap());
assert_eq!(_none.is_none(), true);
}
match匹配必须穷尽所有可能
不能编译:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
我们没有处理 None
的情况,所以这些代码会造成一个 bug。幸运的是,这是一个 Rust 知道如何处理的 bug。如果尝试编译这段代码,会得到这个错误:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
= help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
= note: the matched value is of type `Option<i32>`
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` due to previous error
Rust 知道我们没有覆盖所有可能的情况甚至知道哪些模式被忘记了!Rust 中的匹配是穷举式的(exhaustive):必须穷举到最后的可能性来使代码有效。特别的在这个 Option<T>
的例子中,Rust 防止我们忘记明确的处理 None
的情况,这让我们免于假设拥有一个实际上为空的值,从而使之前提到的价值亿万的错误不可能发生。
通配模式和 _
占位符
对一些特定的值采取特殊操作,而对其他的值采取默认操作
fn add_fancy_hat() {
println!("add_fancy_hat");
}
fn remove_fancy_hat() {
println!("remove_fancy_hat");
}
fn move_player(num_spaces: u8) {
println!("move_player:{}", num_spaces);
}
fn main() {
println!("Hello, world!");
for x in 0..9 {
match x {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
}
}
输出
move_player:0
move_player:1
move_player:2
add_fancy_hat
move_player:4
move_player:5
move_player:6
remove_fancy_hat
move_player:8
对于前两个分支,匹配模式是字面值 3 和 7,最后一个分支则涵盖了所有其他可能的值,模式是我们命名为 other
的一个变量。other
分支的代码通过将其传递给 move_player
函数来使用这个变量。
即使我们没有列出 u8
所有可能的值,这段代码依然能够编译,因为最后一个模式将匹配所有未被特殊列出的值。这种通配模式满足了 match
必须被穷尽的要求。请注意,我们必须将通配分支放在最后,因为模式是按顺序匹配的。如果我们在通配分支后添加其他分支,Rust 将会警告我们,因为此后的分支永远不会被匹配到。
Rust 还提供了一个模式,当我们不想使用通配模式获取的值时,请使用 _
,这是一个特殊的模式,可以匹配任意值而不绑定到该值。这告诉 Rust 我们不会使用这个值,所以 Rust 也不会警告我们存在未使用的变量。
fn add_fancy_hat() {
println!("add_fancy_hat");
}
fn remove_fancy_hat() {
println!("remove_fancy_hat");
}
fn main() {
println!("Hello, world!");
for x in 0..9 {
match x {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
}
}
输出
add_fancy_hat
remove_fancy_hat
明确告诉 Rust 我们不会使用与前面模式不匹配的值,并且这种情况下我们不想运行任何代码。