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 我们不会使用与前面模式不匹配的值,并且这种情况下我们不想运行任何代码。

posted @ 2022-04-23 23:11  DarkH  阅读(198)  评论(0编辑  收藏  举报