【Rust学习】06_2 match控制流

前言

Rust 有一个非常强大的控制流结构,称为 match,它允许你将一个值与一系列模式进行比较,然后根据哪个模式匹配来执行代码。模式可由字面量、变量、通配符和许多其他内容构成;match 的强大之处在于模式的表达性,以及编译器检查,它确保了所有可能的情况都得到处理。

内容

可以把 match 表达式想象成一台硬币分拣机:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样,值也会通过 match 的每一个模式,并且在遇到第一个 “符合” 的模式时,值会进入相关联的代码块并在执行中被使用。

说到硬币,让我们以 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,
    }
}

拆开 value_in_cents 函数中的 match 来看。首先,我们列出 match 关键字,后跟一个表达式,在这个例子中是 coin 的值。这看起来非常像 if 使用的表达式,不过这里有一个非常大的区别:对于 if,表达式必须返回一个布尔值,而这里它可以是任何类型的。例子中的 coin 的类型是我们定义的 Coin 枚举。

接下来是 match 的分支。一个分支有两个部分:一个模式和一些代码。第一个分支的模式是值 Coin::Penny 而之后的 => 运算符将模式和将要运行的代码分开。这里的代码就仅仅是值 1。每一个分支之间使用逗号分隔。

match 表达式执行时,它会按顺序将结果值与每个分支的模式进行比较。如果模式与该值匹配,则执行与该模式关联的代码。如果该模式与值不匹配,则继续执行到下一个分支,就像在硬币分拣机中一样。我们需要多少分支 就有多少:在上方的代码中,我们的 match 有 4 个 分支。

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

如果 match 分支代码很短,我们通常不会使用大括号,就像代码示例中每个分支只返回一个值。如果要在匹配的分支中运行多行代码,则必须使用大括号,并且分支后面的逗号是可选的。例如,以下代码在每次使用 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,
    }
}

绑定值的模式

match 分支的另一个有用功能是它们可以绑定到与模式匹配的值部分。这就是我们从枚举成员中提取值的方法。

例如,让我们更改其中一个枚举成员以在其中保存数据。从 1999 年到 2008 年,美国为 50 个州中的每一个州铸造了一面设计不同的 25 美分硬币。没有其他硬币有国家设计,所以只有 25 美分硬币有这个额外的价值。我们可以通过更改 Quarter 成员来包含存储在其中的 UsState 值,从而将此信息添加到我们的枚举中,我们在下面代码中已经完成了此操作。

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

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

假设一个朋友正在尝试收集所有 50 个州的 25 美分硬币。当我们按硬币类型对零钱进行排序时,我们还会说出与每个 25 美分硬币相关的州名,这样如果我们的朋友没有,他们可以将其添加到他们的收藏中。

在此代码的匹配表达式中,我们将一个名为 state 的变量添加到匹配变体 Coin::Quarter 的值的模式中。当 Coin::Quarter 匹配时,变量 state 将会绑定 25 美分硬币所对应州的值。接着在那个分支的代码中使用 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) 。当我们将该值与每个匹配分支进行比较时,在我们到达 Coin::Quarter(state) 之前,它们都不匹配。此时,state 的绑定将是值 UsState::Alaska。然后,我们可以在 println!表达式中使用该绑定,从而从 QuarterCoin 枚举成员中获取内部状态值。

匹配Option

我们想在使用 Option<T> 时从 Some 情况中获取内部 T 值;我们也可以使用 match 来处理 Option<T>,就像我们对 Coin 枚举所做的那样!我们将比较 Option<T> 的成员,而不是比较硬币,但match表达式的工作方式保持不变。

假设我们想编写一个采用 Option<i32> 的函数,如果里面有一个值,则将该值加 1。如果内部没有值,则函数应返回 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);
}

让我们更详细地研究一下 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 的值加 1 并创建一个新的 Some 值,其中总共为 6

接着考虑下示例中 plus_one 的第二个调用,这里 xNone。我们进入 match 并与第一个分支相比较。

None => None,

匹配上了!这里没有值来加一,所以程序结束并返回 => 右侧的值 None,因为第一个分支就匹配到了,其他的分支将不再比较。

在许多情况下,将 match 和枚举组合在一起很有用。你会在 Rust 代码中经常看到这种模式:match一个枚举,将一个变量绑定到里面的数据,然后基于它执行代码。一开始有点棘手,但一旦你习惯了它,你会希望你拥有所有语言的它。它一直是用户的最爱。

匹配是穷尽的

我们需要讨论match的另一个方面:分支的模式必须涵盖所有可能性。考虑这个版本的 plus_one 函数,它有一个 bug,不会编译:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

们没有处理 None 的情况,所以这段代码会导致一个 bug。幸运的是,这是一个 Rust 知道如何捕获的 bug。如果我们尝试编译此代码,我们将收到此错误:

error[E0004]: non-exhaustive patterns: `Some(_)` not covered
   --> src/main.rs:202:15
    |
202 |         match x {
    |               ^ pattern `Some(_)` not covered
    |
note: `Option<i32>` defined here
   --> /Users/wangyang/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/option.rs:571:1
    |
571 | pub enum Option<T> {
    | ^^^^^^^^^^^^^^^^^^
...
579 |     Some(#[stable(feature = "rust1", since = "1.0.0")] T),
    |     ---- 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
    |
203 ~             None => None,
204 ~             Some(_) => todo!(),
    |

Rust 知道我们没有涵盖所有可能的情况,甚至知道我们忘记了哪个模式!Rust 中的匹配是_穷举式的_:我们必须穷举到最后的可能性才能使代码有效。特别是在 Option<T> 的情况下,当 Rust 防止我们忘记显式处理 None 情况时,它保护了我们避免在可能有 null 时假设我们有一个值,从而使前面讨论的数十亿美元的错误变得不可能。

通配模式和 \_ 占位符

使用枚举,我们还可以为一些特定值采取特殊操作,但对于所有其他值,采取一个默认操作。想象一下,我们正在实现一个游戏,如果您在掷骰子时掷出 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) {}
}

对于前两个分支,模式是文本值 37。对于覆盖所有其他可能值的最后一个分支,模式是我们选择命名为 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 以外的任何值,则轮到您时不会发生任何其他事情。我们可以通过使用单元值(我们在03\_通用编程概念中元组类型一节中提到的空元组类型)作为 _ 分支对应的代码来表示这一点:

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

我们将在后续中介绍更多关于模式和匹配的内容。现在,我们将继续使用 if let 语法,该语法在 match 表达式有点冗长的情况下可能很有用。

posted @ 2024-11-11 14:57:00 王洋 阅读(78) 评论(2)

  1. #1楼 2024-11-14 23:18 wrjwjwjwzl Windows 10.0 Sogou 2.X 美国 纽约 纽约


    你的才华让人惊叹,请继续保持。 http://www.55baobei.com/RImwz3WqZ3.html

  2. #2楼 2024-11-13 17:07 oiibzxalwi Windows 10.0 Sogou 2.X 中国 湖北省 十堰市 电信


    详解步骤:如何成功架设传奇私服教程:https://501h.com/fugu/17326.html

发表评论
昵称
邮箱
网址