Skip to content

枚举和模式匹配

FIXME

枚举的定义

枚举enum

枚举的定义 - Rust 程序设计语言 简体中文版

枚举(Enumerations)是一种用于定义一个类型的值的有限集合的数据结构。枚举类型在Rust中非常强大,它能够帮助开发者更好地表达代码中的概念。因为枚举值只可能是其中一个成员。

在Rust中,使用enum关键字来定义枚举类型。例如,下面是一个表示不同颜色的枚举类型的示例:

enum Color {
    Red,
    Blue,
    Green,
}

在上面的示例中,Color是一个枚举类型,它包含了三个可能的值:RedBlueGreen。这些值被称为枚举的“成员”(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_numberOption<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类型的一些常见操作:

  1. 创建Option值:

    let some_value: Option<i32> = Some(5);  // 使用Some包装具体的值
    let none_value: Option<i32> = None;     // 使用None表示不存在值
    

  2. 解包Option值:

    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
    
    如果你确定Option值一定包含具体的值,可以使用unwrap方法解包。但是如果Option值是None,解包会导致panic。因此,在解包之前最好使用match或者if let进行判断。

  3. 使用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"),
    }
    

  4. 使用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类型用于处理可能不存在的值,它有两个变体:SomeNone

在使用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 的第二个调用,这里 xNone。我们进入 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_valueSome(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_valueNone,则执行else块中的代码。

使用if let可以使代码更简洁和易读,特别是当只关心某个模式的情况下。

    let v = Some(4);
    match v {
        Some(3) => println!("three"),
        _ => ()
    }


    if let Some(3) = v {
        println!("three");
    }

上面代码效果与下面代码效果相同