Rust之枚举和模式匹配(二):控制流程结构match

开发环境

  • Windows 10
  • Rust 1.59.0

 

  •  VS Code 1.65.2 

Rust之枚举和模式匹配(二):控制流程结构match_第1张图片

 项目工程

这里继续沿用上次工程rust-demo

 控制流程结构 - match

Rust有一个非常强大的控制流构造,称为match,它允许您将一个值与一系列模式进行比较,然后根据匹配的模式执行代码。模式可以由文字值、变量名、通配符和许多其他东西组成;后面涵盖了所有不同种类的模式以及它们的作用。match的强大之处在于模式的表现力,以及编译器确认所有可能的情况都被处理的事实。 

把一个match表达式想象成一个硬币分拣机:硬币沿着一条轨道滑下,轨道上有不同大小的洞,每个硬币通过它遇到的第一个洞落入。以同样的方式,值通过match中的每个模式,在第一个模式中,值“适合”,该值落入在执行期间使用的相关代码块中。说到硬币,我们就以使用match为例吧!我们可以编写一个函数,用一种类似于计数机的方法,获取一枚未知的美国硬币,确定它是哪一枚硬币,并返回它的美分值,如下所示:

// 枚举
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

// 函数
fn value_in_cents(coin: Coin) -> u8 {
    // match使用
    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有四个分支。

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

如果匹配分支代码很短,我们通常不使用花括号,如上例所示,其中每个分支只返回一个值。如果你想在一个匹配臂中运行多行代码,你必须使用花括号。例如,下面的代码打印“幸运便士!”每次用Coin::Penny调用方法,但还是返回块的最后一个值,1:

fn value_in_cents(coin: Coin) -> u8 {
    match coin {   // match表达式
        Coin::Penny => {  // 这里返回块
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

绑定到值的模式

match表达式的另一个有用特性是,它们可以绑定到值中与模式匹配的部分。这就是我们如何从枚举变量中提取值。

作为一个例子,让我们改变我们的一个枚举变量来保存数据。从1999年到2008年,美国铸造了50个州不同设计的硬币。没有其他硬币有州图案,所以只有25美分硬币有这种额外的价值。我们可以通过改变Quarter变量来将这个信息添加到我们的enum中,在它里面存储一个UsState值,如下所示:

#[derive(Debug)] // 调试模式
enum UsState {  // 枚举变量
    Alabama,
    Alaska,
    // --snip--
}

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

让我们想象一个朋友正试图收集所有50个州的硬币。当我们按硬币类型对零钱进行分类时,我们还会喊出与每个硬币相关的州名,这样如果我们的朋友没有硬币,他们就可以将其添加到收藏中。

在这段代码的match表达式中,我们将一个名为state的变量添加到匹配变量Coin::Quarter的模式中。当Coin::Quarter匹配时,state变量将绑定到该Quarter的状态值。然后我们可以在分支的代码中使用状态,就像下面这样: 

fn value_in_cents(coin: Coin) -> u8 {
    match coin {    // match表达式
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {     // Quarter
            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时从some中获取内部T值;我们也可以像处理Coin枚举一样使用match来处理Option!我们将比较Option< T >的变体,而不是比较硬币,但是match表达式的工作方式保持不变。

假设我们想写一个函数,它接受一个Option< i32 >,如果里面有一个值,就给这个值加1。如果里面没有值,函数应该返回None值,并且不尝试执行任何操作。

多亏了match,这个函数非常容易编写,如下所示:

fn main() {
    fn plus_one(x: Option) -> Option {
        match x {                // match表达式
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);

    println!("value five = {:#?}", five);    // 输出类型
    println!("value six = {:#?}", six);
    println!("value none = {:#?}", none);
}

编译运行

cargo run

Rust之枚举和模式匹配(二):控制流程结构match_第2张图片

让我们更详细地检查一下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。然后执行match分支中的代码,因此我们将i的值加1,并创建一个新的Some值,其中包含我们的总计6

现在让我们考虑上例种对plus_one的第二次调用,其中xNone。我们进入match,并与第一条进行比较,

None => None,

完全吻合,没有要添加的值,所以程序停止并返回=>右侧的None值。因为第一条匹配,所以不比较其他分支。

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

匹配是详尽的

我们还需要讨论match的另一个方面。考虑一下这个版本的plus_one函数,它有一个错误,不能编译:

fn main() {
    fn plus_one(x: Option) -> Option {
        match x {          // match表达式
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

我们没有处理None的情况,所以这段代码会导致一个bug。幸运的是,这是一只锈菌知道如何捕捉的虫子。如果我们试图编译这段代码,我们会得到这个错误:

编译

cargo run

Rust之枚举和模式匹配(二):控制流程结构match_第3张图片

Rust知道我们没有涵盖所有可能的情况,甚至知道我们忘记了哪个模式!Rust中的匹配是穷尽的:为了使代码有效,我们必须穷尽所有的可能性。特别是在Option的情况下,当Rust防止我们忘记显式处理None的情况时,它防止我们在可能有null时假设我们有一个值,从而使前面讨论的十亿美元的错误不可能发生。 

包罗万象的模式和 _占位符

使用枚举,我们还可以对一些特定的值采取特殊的操作,但是对所有其他的值采取一个默认的操作。想象一下,我们正在实现一个游戏,如果你掷骰子掷出一个3,你的玩家不动,而是得到一顶新的漂亮的帽子。如果你掷出7,你的玩家将失去一顶漂亮的帽子。对于所有其他值,你的玩家在棋盘上移动该数量的空格。这里有一个实现该逻辑的match,骰子滚动的结果是硬编码的,而不是随机值,所有其他逻辑都由没有主体的函数表示,因为实际实现它们超出了本例的范围:

let dice_roll = 9;
    match dice_roll {    // match表达式
        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还有一个模式,当我们不想使用catch-all模式中的值时,我们可以使用这个模式:_,这是一个特殊的模式,它匹配任何值,并且不绑定到那个值。这告诉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() {}

这个例子也满足了穷举要求,因为我们显式地忽略了最后一个分支中的所有其他值;我们什么都没忘记。

如果我们再改变一次游戏规则,如果你掷出的不是3或7,那么在你的回合中不会发生任何事情,我们可以通过使用单位值(元组类型”一节中提到的空元组类型)作为_ 分支的代码来表达这一点:

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表达式有点冗长的情况下很有用。

本章重点

  • match表达式
  • 绑定到值的模式
  • _占位符

你可能感兴趣的:(Rust,rust)