键盘快捷键

按 ← 或 → 在章节间切换

按 S 或 / 搜索本书

按 ? 显示此帮助

按 Esc 隐藏此帮助

match 控制流结构

Rust 提供了一种功能极其强大的控制流结构,称为 match。它可以将一个值与一系列模式进行比较,然后根据匹配的模式执行代码。模式可以由字面值、变量名、通配符以及许多其他内容组成;第 19 章会介绍各种模式及其作用。match 的强大之处在于模式表达能力丰富,而且编译器会确认所有可能的情况都已处理。

可以把 match 表达式想象成一台硬币分拣机:硬币沿着一条轨道滑下,轨道上有大小不同的孔,每枚硬币都会从遇到的第一个能通过的孔中落下。同样,值会依次与 match 中的每个模式进行比较;一旦某个模式与值“匹配”,该值就会进入对应的代码块并在执行时使用。

说到硬币,我们就用硬币作为 match 的示例!我们可以编写一个函数,接收一枚未知的美国硬币,并像分拣机一样判断它是哪种硬币,然后返回其美分面值,如示例 6-3 所示。

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,
    }
}

fn main() {}
示例 6-3:一个枚举及其 match 表达式,其中枚举变体用作匹配模式

来拆解一下 value_in_cents 函数中的 match。首先写出关键字 match,后面跟一个表达式,此处是 coin 的值。这看起来与 if 条件表达式很相似,但有一个重要区别:if 的条件必须计算为布尔值,而这里可以是任何类型。本例中 coin 的类型是第一行定义的 Coin 枚举。

接下来是 match 分支。每个分支由两部分组成:一个模式和一些代码。这里第一个分支的模式是 Coin::Penny,后面使用 => 运算符分隔模式与要运行的代码。此处的代码只有值 1。每个分支之间用逗号分隔。

执行 match 表达式时,会按顺序将结果值与各分支的模式进行比较。如果模式匹配该值,就执行与模式关联的代码;如果不匹配,就像硬币分拣机一样继续检查下一个分支。分支数量可以按需增加:示例 6-3 中的 match 有四个分支。

每个分支关联的代码都是一个表达式,而匹配分支中表达式的结果值,就是整个 match 表达式的返回值。

如果匹配分支中的代码很短,通常不使用花括号,例如示例 6-3 中每个分支都只返回一个值。如果希望在一个分支中运行多行代码,就必须使用花括号;此时分支后面的逗号可以省略。例如,下面的代码每次使用 Coin::Penny 调用该方法时都会打印 “Lucky penny!”,但仍返回代码块的最后一个值 1:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}

绑定值的模式

match 分支的另一个实用特性是可以将匹配值中的部分内容绑定到变量上。我们可以借此从枚举变体中提取值。

例如,我们来修改一个枚举变体,让它包含数据。1999 年至 2008 年间,美国铸造的 25 美分硬币背面采用了代表 50 个州的不同图案。其他硬币没有州的图案,因此只有 25 美分硬币带有这个额外值。我们可以修改 enum,让 Quarter 变体包含一个 UsState 值,如示例 6-4 所示。

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {}
示例 6-4:Coin 枚举的 Quarter 变体还包含一个 UsState 值

假设有位朋友想集齐代表 50 个州的 25 美分硬币。我们按硬币种类整理零钱时,也会说出每枚 25 美分硬币所代表的州名;如果朋友还没有这枚硬币,就可以把它加入收藏。

在这段代码的 match 表达式中,我们在匹配 Coin::Quarter 变体值的模式里添加了一个名为 state 的变量。当 Coin::Quarter 匹配时,state 变量就会绑定到该枚硬币所代表的州值。然后,我们可以在该分支的代码中使用 state,如下所示:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    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() {
    value_in_cents(Coin::Quarter(UsState::Alaska));
}

如果调用 value_in_cents(Coin::Quarter(UsState::Alaska)),那么 coin 的值就是 Coin::Quarter(UsState::Alaska)。将该值与每个 match 分支进行比较时,直到遇到 Coin::Quarter(state) 才会匹配。此时,state 绑定的值就是 UsState::Alaska。随后我们可以在 println! 表达式中使用这个绑定,从而取出 Coin 枚举的 Quarter 变体中包含的州值。

Option<T> 的 match 模式

上一节中,我们希望在使用 Option<T> 时从 Some 情况中取出内部的 T 值;我们也可以像处理 Coin 枚举那样,使用 match 处理 Option<T>!区别只是这里匹配的是 Option<T> 的变体,而 match 表达式的工作方式仍然相同。

假设我们想编写一个函数,接收 Option<i32>,如果其中包含值,就将该值加 1;如果不包含值,函数就应返回 None,而不尝试执行任何操作。

得益于 match,这个函数很容易编写,如示例 6-5 所示。

fn main() {
    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);
}
示例 6-5:使用 match 表达式处理 Option<i32> 的函数

我们来更详细地看看第一次执行 plus_one 的过程。调用 plus_one(five) 时,plus_one 函数体中的变量 x 将具有值 Some(5)。然后,我们将它与每个 match 分支进行比较:

fn main() {
    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);
}

Some(5) 值与模式 None 不匹配,因此继续检查下一个分支:

fn main() {
    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);
}

Some(5) 与 Some(i) 匹配吗?匹配!它们是相同的变体。i 会绑定到 Some 中包含的值,因此 i 的值为 5。随后执行该匹配分支中的代码,将 i 的值加 1,并创建一个内部值为 6 的新 Some 值。

现在来看示例 6-5 中第二次调用 plus_one 的情况,此时 x 为 None。我们进入 match,并将其与第一个分支进行比较:

fn main() {
    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);
}

它匹配了!没有值可以相加,因此程序会停止,并返回 => 右侧的 None 值。由于第一个分支已经匹配,不会再比较其他分支。

在许多情况下,将 match 与枚举结合起来都很有用。你会在 Rust 代码中经常看到这种模式:匹配枚举,将变量绑定到其中的数据,然后据此执行代码。起初可能有点难理解,但熟悉之后,你会希望所有语言都具备这一功能。它一直深受用户喜爱。

匹配必须穷尽所有情况

关于 match 还有一个方面需要讨论:分支中的模式必须覆盖所有可能性。看下面这个有缺陷且无法编译的 plus_one 函数版本:

fn main() {
    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);
}

我们没有处理 None 情况,因此这段代码会导致错误。幸运的是,Rust 能够捕获这种错误。尝试编译这段代码时,会得到以下错误:

$ 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
  |
note: `Option<i32>` defined here
 --> /rustc/2d8144b7880597b6e6d3dfd63a9a9efae3f533d3/library/core/src/option.rs:597:0
 ::: /rustc/2d8144b7880597b6e6d3dfd63a9a9efae3f533d3/library/core/src/option.rs:601:4
  |
  = note: not covered
  = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
  |
4 ~             Some(i) => Some(i + 1),
5 ~             None => todo!(),
  |

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error

Rust 知道我们没有覆盖所有可能情况,甚至知道遗漏了哪个模式!Rust 中的 match 是穷尽的:代码要有效,就必须覆盖每一种可能性。尤其对于 Option<T>,Rust 会阻止我们忘记显式处理 None,从而避免我们在可能没有值时误以为值存在,也就杜绝了前面提到的价值十亿美元的错误。

全匹配模式与 _ 占位符

使用枚举时,我们也可以对少数特定值采取特殊操作,并对所有其他值执行一个默认操作。假设我们正在实现一款游戏:掷出 3 时,玩家不移动,而是获得一顶漂亮的新帽子;掷出 7 时,玩家会失去一顶漂亮的帽子;其他点数则让玩家在棋盘上移动相应格数。下面的 match 实现了这一逻辑,其中骰子结果是硬编码的,而不是随机值;其他逻辑则用没有函数体的函数表示,因为实际实现它们不属于本示例的范围:

fn main() {
    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) {}
}

前两个分支的模式是字面值 3 和 7。覆盖所有其他可能值的最后一个分支,其模式是我们命名为 other 的变量。other 分支中的代码通过将该变量传给 move_player 函数来使用它。

即使我们没有列出 u8 的所有可能值,这段代码仍然可以编译,因为最后一个模式会匹配所有未明确列出的值。这个全匹配模式满足了 match 必须穷尽所有情况的要求。请注意,全匹配分支必须放在最后,因为模式按顺序求值。如果把全匹配分支放在前面,其他分支就永远不会运行,因此 Rust 会在全匹配分支之后还有其他分支时发出警告!

如果我们想使用全匹配模式,却不想使用匹配到的值,Rust 还提供了 _ 这一特殊模式。它可以匹配任何值,但不会将值绑定到变量上。这样就告诉 Rust 我们不会使用该值,因此 Rust 不会因为未使用变量而发出警告。

我们再修改一下游戏规则:现在,掷出 3 或 7 以外的任何点数时,都必须重新掷骰子。我们不再需要使用全匹配值,因此可以用 _ 代替名为 other 的变量:

fn main() {
    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() {}
}

这个示例也满足穷尽性要求,因为我们在最后一个分支中明确忽略了所有其他值,没有遗漏任何情况。

最后,我们再修改一次游戏规则:如果掷出的点数不是 3 或 7,本回合就不发生其他事情。我们可以将单元值(即“元组类型”一节中介绍的空元组类型)用作 _ 分支的代码来表达这一点:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
}

这里我们明确告诉 Rust:对于前面的分支都未匹配的其他值,我们既不打算使用,也不希望执行任何代码。

关于模式和匹配的更多内容将在第 19 章介绍。目前我们将转向 if let 语法;当 match 表达式显得有些冗长时,它会很有用。