Rust-模式匹配

本文上半部分会介绍各种不同类型模式的例子帮助学习,而从第四章开始,会把常用的模式语法都一一罗列出来,方便在未来的开发生涯中随时检索查阅。

汉语字典中对“模式”的解释是:事物的标准样式。在计算机科学中,它指特定类型的数据(往往是序列或是树形结构)满足某一特定结构或格式。“匹配”本身是指一个判断寻找过程。最早的模式匹配用于文本编辑器中的正则字符串搜索,之后才作为编程语言特性。

Rust 拥有着高级且设计良好的模式系统。当然,这也要归功于 Rust 语言设计独有的严谨标准。

模式是 Rust 中的特殊语法,它用来匹配类型中的结构和数据,它往往和 match​ 表达式联用,以实现强大的模式匹配能力。模式一般由以下内容组合而成:

  • 字面值
  • 解构的数组、枚举、结构体或者元组
  • 变量
  • 通配符
  • 占位符

一、match 和 if let

在 Rust 中,模式匹配最常用的就是 match​ 和 if let​,本节将对两者及相关的概念进行详尽介绍。

先来看一个关于 match​ 的简单例子:

enum Direction {
    East,
    West,
    North,
    South,
}

fn main() {
    let dire = Direction::South;
    match dire {
        Direction::East => println!("East"),
        Direction::North | Direction::South => {
            println!("South or North");
        },
        _ => println!("West"),
    };
}

这里我们想去匹配 dire​ 对应的枚举类型,因此在 match​ 中用三个匹配分支来完全覆盖枚举变量 Direction​ 的所有成员类型,有以下几点值得注意:

  • match​ 的匹配必须要穷举出所有可能,因此这里用 _​ 来代表未列出的所有可能性
  • match​ 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同
  • X | Y,类似逻辑运算符 ​,代表该分支可以匹配 X​ 也可以匹配 Y​,只要满足一个即可

其实 match​ 跟其他语言中的 switch​ 非常像,_​ 类似于 switch​ 中的 default​。

1. match 匹配

首先来看看 match​ 的通用形式:

match target {
    模式1 => 表达式1,
    模式2 => {
        语句1;
        语句2;
        表达式2
    },
    _ => 表达式3
}

该形式清晰的说明了何为模式,何为模式匹配:将模式与 target​ 进行匹配,即为模式匹配,而模式匹配不仅仅局限于 match​,后面我们会详细阐述。

match​ 允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行对应的代码,下面让我们来一一详解,先看一个例子:

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

在美元中存在四种不同面值的硬币:Penny、Nickel、Dime、Quarter,它们分别代表1、5、10、25美分。

在上述代码中,value_in_cents​ 函数根据匹配到的硬币,返回对应的美分数值。match​ 后紧跟着的是一个表达式,跟 if​ 很像,但是 if​ 后的表达式必须是一个布尔值,而 match​ 后的表达式返回值可以是任意类型,只要能跟后面的分支中的模式匹配起来即可,这里的 coin​ 是枚举 Coin​ 类型。

接下来是 match​ 的分支。一个分支有两个部分:一个模式和针对该模式的处理代码。 第一个分支的模式是 Coin::Penny​,其后的 =>​ 运算符将模式和将要运行的代码分开。这里的代码就仅仅是表达式 1​,不同分支之间使用逗号分隔。

match​ 表达式执行时,它将目标值 coin​ 按顺序依次与每一个分支的模式相比较,如果模式匹配了这个值,那么模式之后的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支。

每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 match​ 表达式的返回值。如果分支有多行代码,那么需要用 {}​ 包裹,同时最后一行代码需要是一个表达式。

(1)使用 match 表达式赋值

还有一点很重要,match​ 本身也是一个表达式,因此可以用它来赋值:

enum IpAddr {
   Ipv4,
   Ipv6
}

fn main() {
    let ip1 = IpAddr::Ipv6;
    let ip_str = match ip1 {
        IpAddr::Ipv4 => "127.0.0.1",
        _ => "::1",
    };

    println!("{}", ip_str);
}

因为这里匹配到 _​ 分支,所以将 "::1"​ 赋值给了 ip_str​。

(2)模式绑定

模式匹配的另外一个重要功能是从模式中取出绑定的值,例如:

enum UsState {
    Alabama,
    Alaska,
    // ...48个其他州的名字
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState), // 25美分硬币
}

其中 Coin::Quarter​ 成员还存放了一个值:美国的某个州(因为在 1999 年到 2008 年间,美国在 25 美分(Quarter)硬币的背后为 50 个州印刷了不同的标记,其它硬币都没有这样的设计)。

接下来,我们希望在模式匹配中,获取到 25 美分硬币上刻印的州的名称:

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

上面代码中,在匹配 Coin::Quarter(state)​ 模式时,我们把它内部存储的值绑定到了 state​ 变量上,因此 state​ 变量就是对应的 UsState​ 枚举类型。

例如有一个印了阿拉斯加州标记的 25 分硬币:Coin::Quarter(UsState::Alaska)​, 它在匹配时,state​ 变量将被绑定 UsState::Alaska​ 的枚举值。

就像下面这样:

fn main() {
    #[derive(Debug)]
    enum UsState {
        Alabama,
        Alaska,
        // ...48个其他州的名字
    }
    enum Coin {
        Penny,
        Nickel,
        Dime,
        Quarter(UsState), // 25美分硬币
    }

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

    let alaska_quarter = Coin::Quarter(UsState::Alaska);
    println!("Value of Alaska quarter: {}", value_in_cents(alaska_quarter));
}

// 运行结果:
// State quarter from Alaska!
// Value of Alaska quarter: 25

再来看一个更复杂的例子:

/// 枚举类型,表示一个动作
enum Action {
    /// 说话动作,传入一个字符串
    Say(String),
    /// 移动动作,传入两个整数,表示x和y坐标
    MoveTo(i32, i32),
    /// 改变颜色动作,传入三个无符号16位整数,表示RGB颜色值
    ChangeColorRGB(u16, u16, u16),
}

/// 主函数
fn main() {
    /// 定义一个动作数组
    let actions = [
        /// 说话动作,传入字符串"Hello Rust"
        Action::Say("Hello Rust".to_string()),
        /// 移动动作,传入整数1和2
        Action::MoveTo(1,2),
        /// 改变颜色动作,传入整数255, 255, 0
        Action::ChangeColorRGB(255,255,0),
    ];
    /// 遍历动作数组
    for action in actions {
        /// 使用match语句对动作进行匹配
        match action {
            /// 如果是说话动作,打印传入的字符串
            Action::Say(s) => {
                println!("{}", s);
            },
            /// 如果是移动动作,打印传入的x和y坐标
            Action::MoveTo(x, y) => {
                println!("point from (0, 0) move to ({}, {})", x, y);
            },
            /// 如果是改变颜色动作,打印传入的r和g值,忽略b值
            Action::ChangeColorRGB(r, g, _) => {
                println!("change color into '(r:{}, g:{}, b:0)', 'b' has been ignored",
                         r, g,
                );
            }
        }
    }
}

运行后输出:

Hello Rust
point from (0, 0) move to (1, 2)
change color into '(r:255, g:255, b:0)', 'b' has been ignored
(3)穷尽匹配

在文章的开头,我们简单总结过 match​ 的匹配必须穷尽所有情况,下面来举例说明,例如:

enum Direction {
    East,
    West,
    North,
    South,
}

fn main() {
    let dire = Direction::South;
    match dire {
        Direction::East => println!("East"),
        Direction::North | Direction::South => {
            println!("South or North");
        },
    };
}

在上述代码中我们没有处理 Direction::West​ 的情况,就会报下面这个错误:

error[E0004]: non-exhaustive patterns: ‘West’ not covered
// 非穷尽匹配,‘West’ 没有被覆盖

Rust 的编译器在诸如此类的检查上面能力非常强大。

(4)通配符 _

当我们不想在匹配时列出所有值的时候,可以使用 Rust 提供的一个特殊模式,例如,u8​ 可以拥有 0 到 255 的有效的值,但是我们只关心 1、3、5 和 7​ 这几个值,不想列出其它的 0、2、4、6、8、9 一直到 255​ 的值。那么, 我们不必一个一个列出所有值, 因为可以使用特殊的模式 _​ 替代:

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

通过将 _​ 其放置于其他分支后,_​ 将会匹配所有遗漏的值。()​ 表示返回单元类型与所有分支返回值的类型相同,所以当匹配到 _​ 后,什么也不会发生。

除了_​通配符,用一个变量来承载其他情况也是可以的。

#[derive(Debug)]
enum Direction {
    East,
    West,
    North,
    South,
}

fn main() {
    let dire = Direction::South;
    match dire {
        Direction::East => println!("East"),
        other => println!("other direction: {:?}", other),
    };
}

然而,在某些场景下,我们其实只关心某一个值是否存在,此时 match​ 就显得过于啰嗦,就要用到 if let​。

2. if let 匹配

有时会遇到只有一个模式的值需要被处理,其它值直接忽略的场景,如果用 match​ 来处理就要写成下面这样:

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

代码声明了一个变量 v​,并将其初始化为 Option<u8>​ 类型的 Some​ 变体,该变体包含了一个 u8​ 类型的值 3​。这里的 u8​ 表示一个无符号8位整数。

我们只想要对 Some(3)​ 模式进行匹配, 不想处理任何其他 Some<u8>​ 值或 None​ 值。但是为了满足 match​ 表达式(穷尽性)的要求,写代码时必须在处理完这唯一的成员后加上 _ => ()​,这样会增加不少无用的代码。

俗话说“杀鸡焉用牛刀”,我们完全可以用 if let​ 的方式来实现:

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

这两种匹配对于新手来说,可能有些难以抉择,但是只要记住一点就好:当你只要匹配一个条件,且忽略其他条件时就用 if let ,否则都用 match

3. matches! 宏

Rust 标准库中提供了一个非常实用的宏:matches!​,它可以将一个表达式跟模式进行匹配,然后返回匹配的结果 true​ or false​。

例如,有一个动态数组,里面存有以下枚举:

enum MyEnum {
    Foo,
    Bar
}

fn main() {
    let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
}

现在如果想对 v​ 进行过滤,只保留类型是 MyEnum::Foo​ 的元素,你可能想这么写:

v.iter().filter(|x| x == MyEnum::Foo);

但是,实际上这行代码会报错,因为你无法将 x​ 直接跟一个枚举成员进行比较。好在,你可以使用 match​ 来完成,但是会导致代码更为啰嗦,是否有更简洁的方式?答案是使用 matches!​:

v.iter().filter(|x| matches!(x, MyEnum::Foo));

很简单也很简洁,再来看看更多的例子:

let foo = 'f';
assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));

let bar = Some(4);
assert!(matches!(bar, Some(x) if x > 2));

4. 变量遮蔽

无论是 match​ 还是 if let​,这里都是一个新的代码块,而且这里的绑定相当于新变量,如果你使用同名变量,会发生变量遮蔽:

fn main() {
   let age = Some(30);
   println!("在匹配前,age是{:?}",age);
   if let Some(age) = age {
       println!("匹配出来的age是{}",age);
   }

   println!("在匹配后,age是{:?}",age);
}

运行后输出如下:

在匹配前,age是Some(30)
匹配出来的age是30
在匹配后,age是Some(30)

可以看出在 if let​ 中,=​ 右边 Some(i32)​ 类型的 age​ 被左边 i32​ 类型的新 age​ 遮蔽了,该遮蔽一直持续到 if let​ 语句块的结束。因此第三个 println!​ 输出的 age​ 依然是 Some(i32)​ 类型。

对于 match​ 类型也是如此:

fn main() {
   let age = Some(30);
   println!("在匹配前,age是{:?}",age);
   match age {
       Some(age) =>  println!("匹配出来的age是{}",age),
       _ => ()
   }
   println!("在匹配后,age是{:?}",age);
}

需要注意的是 match 中的变量遮蔽其实不是那么的容易看出,因此要小心!其实这里最好不要使用同名,避免难以理解,如下:

fn main() {
   let age = Some(30);
   println!("在匹配前,age是{:?}", age);
   match age {
       Some(x) =>  println!("匹配出来的age是{}", x),
       _ => ()
   }
   println!("在匹配后,age是{:?}", age);
}

二、解构 Option

在枚举那章,提到过 Option​ 枚举,它用来解决 Rust 中变量是否有值的问题,定义如下:

enum Option<T> {
    Some(T),
    None,
}

简单解释就是:一个变量要么有值:Some(T) ​,要么为空:None

那么现在的问题就是该如何去使用这个 Option​ 枚举类型,根据我们上一节的经验,可以通过 match​ 来实现。

因为 Option​,Some​,None​ 都包含在 prelude​ 中,因此你可以直接通过名称来使用它们,而无需以 Option::Some​ 这种形式去使用,总之,千万不要因为调用路径变短了,就忘记 Some​ 和 None​ 也是 Option​ 底下的枚举成员!

匹配 Option

使用 Option<T>​,是为了从 Some​ 中取出其内部的 T​ 值以及处理没有值的情况,为了演示这一点,下面一起来编写一个函数,它获取一个 Option<i32>​,如果其中含有一个值,将其加一;如果其中没有值,则函数返回 None​ 值:

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​ 接受一个 Option<i32>​ 类型的参数,同时返回一个 Option<i32>​ 类型的值(这种形式的函数在标准库内随处所见),在该函数的内部处理中,如果传入的是一个 None​ ,则返回一个 None​ 且不做任何处理;如果传入的是一个 Some(i32)​,则通过模式绑定,把其中的值绑定到变量 i​ 上,然后返回 i+1​ 的值,同时用 Some​ 进行包裹。

为了进一步说明,假设 plus_one​ 函数接受的参数值 x 是 Some(5)​,来看看具体的分支匹配情况:

传入参数 Some(5)
None => None,

首先是匹配 None​ 分支,因为值 Some(5)​ 并不匹配模式 None​,所以继续匹配下一个分支。

Some(i) => Some(i + 1),

Some(5)​ 与 Some(i)​ 匹配吗?当然匹配!它们是相同的成员。i​ 绑定了 Some​ 中包含的值,因此 i​ 的值是 5​。接着匹配分支的代码被执行,最后将 i​ 的值加一并返回一个含有值 6​ 的新 Some​。

传入参数 None

接着考虑下 plus_one​ 的第二个调用,这次传入的 x​ 是 None​, 我们进入 match​ 并与第一个分支相比较。

None => None,

匹配上了!接着程序继续执行该分支后的代码:返回表达式 None​ 的值,也就是返回一个 None​,因为第一个分支就匹配到了,其他的分支将不再比较。

三、所有可能会用到模式的位置

模式出现在 Rust 的很多地方。本部分是一个所有有效模式位置的参考。

1. match 分支

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

如上所示,match​ 的每个分支就是一个模式,因为 match​ 匹配是穷尽式的,因此我们往往需要一个特殊的模式 _​,来匹配剩余的所有情况:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    _ => EXPRESSION,
}

2. if let 分支

if let​ 往往用于匹配一个模式,而忽略剩下的所有模式的场景:

if let PATTERN = SOME_VALUE {

}

也可以在 if let​ 中包含一个 else​。

else​ 块中的代码与 match​ 表达式中的 _​ 分支块中的代码相同,这样的 match​ 表达式就等同于 if let​ 和 else​:

if let PATTERN = SOME_VALUE {
    //表达式...
} else {
    //表达式...
}

3. while let 条件循环

一个与 if let​ 类似的结构是 while let​ 条件循环,它允许只要模式匹配就一直进行 while​ 循环。下面展示了一个使用 while let​ 的例子:

// Vec是动态数组
let mut stack = Vec::new();

// 向数组尾部插入元素
stack.push(1);
stack.push(2);
stack.push(3);

// stack.pop从数组尾部弹出元素
while let Some(top) = stack.pop() {
    println!("{}", top);
}

这个例子会打印出 3​、2​ 接着是 1​。pop​ 方法取出动态数组的最后一个元素并返回 Some(value)​,如果动态数组是空的,将返回 None​,对于 while​ 来说,只要 pop​ 返回 Some​ 就会一直不停的循环。一旦其返回 None​,while​ 循环停止。我们可以使用 while let​ 来弹出栈中的每一个元素。

你也可以用 loop​ + if let​ 或者 match​ 来实现这个功能,但是会更加啰嗦。

4. for 循环

for​ 循环是 Rust 中最常见的循环结构,不过还没有讲到的是 for​ 可以获取一个模式。在 for​ 循环中,模式是 for​ 关键字直接跟随的值,正如 for x in y​ 中的 x​。

let v = vec!['a', 'b', 'c'];

for (index, value) in v.iter().enumerate() {
    println!("{} is at index {}", value, index);
}

这里使用 enumerate​ 方法产生一个迭代器,该迭代器每次迭代会返回一个 (索引,值)​ 形式的元组,然后用 (index,value)​ 来匹配。

5. let 语句

let PATTERN = EXPRESSION;

是的, 该语句我们已经用了无数次了,它也是一种模式匹配:

let x = 5;

这其中,x​ 也是一种模式绑定,代表将匹配的值绑定到变量 x 上。因此,在 Rust 中,变量名也是一种模式,只不过它比较朴素很不起眼罢了。

let (x, y, z) = (1, 2, 3);

上面将一个元组与模式进行匹配(模式和值的类型必需相同!),然后把 1, 2, 3​ 分别绑定到 x, y, z​ 上。

6. 函数参数

函数参数也是模式,以下代码声明了一个叫做 foo​ 的函数,它获取一个 i32​ 类型的参数 x​,:

fn foo(x: i32) {
    // 代码
}

其中 x​ 就是一个模式,你还可以在参数中匹配元组:

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

&(3, 5)​ 会匹配模式 &(x, y)​,因此 x​ 得到了 3​,y​ 得到了 5​。

7. let 和 if let

对于以下代码,编译器会报错:

let Some(x) = some_option_value;

因为右边的值可能不为 Some​,而是 None​,这种时候就不能进行匹配,也就是上面的代码遗漏了对 some_option_value 为 None​ 时的匹配。

类似 let​ , for​和match​ 都必须要求完全覆盖匹配,才能通过编译( 不可驳模式匹配 )。

但是对于 if let​,就可以这样使用:

if let Some(x) = some_option_value {
    println!("{}", x);
}

因为 if let​ 允许匹配一种模式,而忽略其余的模式( 可驳模式匹配 )。

四、全模式列表(模式语法)

1. 匹配字面值

let x = 1;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    _ => println!("anything"),
}

这段代码会打印 one​ 因为 x​ 的值是 1,如果希望代码获得特定的具体值,那么这种语法很有用。

2. 匹配命名变量

在 match 中,我们有讲过变量遮蔽的问题,这个在匹配命名变量时会遇到:

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {:?}", y),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {:?}", x, y);
}

让我们看看当 match​ 语句运行的时候发生了什么。

第一个匹配分支的模式与 x​ 中定义的值并不匹配,所以代码继续执行。

第二个匹配分支中的模式引入了一个新变量 y​,它会匹配任何 Some​ 中的值。因为这里的 y​ 在 match​ 表达式的作用域中,而不是之前 main​ 作用域中,所以这是一个新变量,不是开头声明为值 10 的那个 y​。

这个新的 y​ 绑定会匹配任何 Some​ 中的值,在这里是 x​ 中的值。因此这个 y​ 绑定了 x​ 中 Some​ 内部的值。这个值是 5,所以这个分支的表达式将会执行并打印出 Matched,y = 5​。

如果 x​ 的值是 None​ 而不是 Some(5)​,那么前两个分支的模式不会匹配,所以会匹配模式 _​。这个分支的模式中没有引入变量 x​,所以此时表达式中的 x​ 会是外部没有被遮蔽的 x​,也就是 None​。

一旦 match​ 表达式执行完毕,其作用域也就结束了,同理内部 y​ 的作用域也结束了。最后的 println!​ 会打印 at the end: x = Some(5), y = 10​。

如果你不想引入变量遮蔽,可以使用另一个变量名而非 y​,或者使用匹配守卫(match guard)的方式,稍后在下面匹配守卫提供的额外条件​中会讲解。

3. 单分支多模式

match​ 表达式中,可以使用 |​ 语法匹配多个模式,它代表 的意思。例如,如下代码将 x​ 的值与匹配分支相比较,第一个分支有 选项,意味着如果 x​ 的值匹配此分支的任何一个模式,它就会运行:

let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

上面的代码会打印 one or two​。

4. 通过序列..=​匹配值的范围

for循环 中我们有讲到一个序列语法,该语法不仅可以用于循环中,还能用于匹配模式。

..=​ 语法允许你匹配一个闭区间序列内的值。在如下代码中,当模式匹配任何在此序列内的值时,该分支会执行:

let x = 5;

match x {
    1..=5 => println!("one through five"),
    _ => println!("something else"),
}

如果 x​ 是 1、2、3、4 或 5,第一个分支就会匹配。这相比使用 |​ 运算符表达相同的意思更为方便;相比 1..=5​,使用 |​ 则不得不指定 1 | 2 | 3 | 4 | 5​ 这五个值,而使用 ..=​ 指定序列就简短的多,比如希望匹配比如从 1 到 1000 的数字的时候!

序列只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型。

如下是一个使用字符类型序列的例子:

let x = 'c';

match x {
    'a'..='j' => println!("early ASCII letter"),
    'k'..='z' => println!("late ASCII letter"),
    _ => println!("something else"),
}

Rust 知道 'c'​ 位于第一个模式的序列内,所以会打印出 early ASCII letter​。

5. 解构并分解值

也可以使用模式来解构结构体、枚举、元组、数组和引用。

(1)解构结构体

下面代码展示了如何用 let​ 解构一个带有两个字段 x​ 和 y​ 的结构体 Point​:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}

这段代码创建了变量 a​ 和 b​ 来匹配结构体 p​ 中的 x​ 和 y​ 字段,这个例子展示了模式中的变量名不必与结构体中的字段名一致。不过通常希望变量名与字段名一致以便于理解变量来自于哪些字段。

因为变量名匹配字段名是常见的,同时因为 let Point { x: x, y: y } = p;​ 中 x​ 和 y​ 重复了,所以对于匹配结构体字段的模式存在简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称。下例与上例有着相同行为的代码,不过 let​ 模式创建的变量为 x​ 和 y​ 而不是 a​ 和 b​:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}

这段代码创建了变量 x​ 和 y​,与结构体 p​ 中的 x​ 和 y​ 字段相匹配。其结果是变量 x​ 和 y​ 包含结构体 p​ 中的值。

也可以使用字面值作为结构体模式的一部分进行解构,而不是为所有的字段创建变量。这允许我们测试一些字段为特定值的同时创建其他字段的变量。

下文展示了固定某个字段的匹配方式:

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {}", x),
        Point { x: 0, y } => println!("On the y axis at {}", y),
        Point { x, y } => println!("On neither axis: ({}, {})", x, y),
    }
}

首先是 match​ 第一个分支,指定匹配 y​ 为 0​ 的 Point​;

然后第二个分支在第一个分支之后,匹配 y​ 不为 0​,x​ 为 0​ 的 Point​;

最后一个分支匹配 x​ 不为 0​,y​ 也不为 0​ 的 Point​。

在这个例子中,值 p​ 因为其 x​ 包含 0 而匹配第二个分支,因此会打印出 On the y axis at 7​。

(2)解构枚举

下面代码以 Message​ 枚举为例,编写一个 match​ 使用模式解构每一个内部值:

enum Message {
    // 退出消息
    Quit,
    // 移动消息,包含x和y两个整数
    Move { x: i32, y: i32 },
    // 写入消息,包含一个字符串
    Write(String),
    // 颜色改变消息,包含红绿蓝三个整数
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        // 如果是退出消息
        Message::Quit => {
            // 打印退出消息
            println!("The Quit variant has no data to destructure.")
        }
        // 如果是移动消息
        Message::Move { x, y } => {
            // 打印移动消息
            println!("Move in the x direction {} and in the y direction {}", x, y);
        }
        // 如果是写入消息
        Message::Write(text) => println!("Text message: {}", text),
        // 如果是颜色改变消息
        Message::ChangeColor(r, g, b) => {
            // 打印颜色改变消息
            println!("Change the color to red {}, green {}, and blue {}", r, g, b)
        }
    }
}

这里老生常谈一句话,模式匹配一样要类型相同,因此匹配 Message::Move{1,2}​ 这样的枚举值,就必须要用 Message::Move{x,y}​ 这样的同类型模式才行。

这段代码会打印出 Change the color to red 0, green 160, and blue 255​。尝试改变 msg​ 的值来观察其他分支代码的运行。

对于像 Message::Quit​ 这样没有任何数据的枚举成员,不能进一步解构其值。只能匹配其字面值 Message::Quit​,因此模式中没有任何变量。

对于另外两个枚举成员,就用相同类型的模式去匹配出对应的值即可。

(3)解构嵌套的结构体和枚举

目前为止,所有的例子都只匹配了深度为一级的结构体或枚举。 match​ 也可以匹配嵌套的项!

例如使用下面的代码来同时支持 RGB 和 HSV 色彩模式:

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => {
            println!("Change the color to red {}, green {}, and blue {}", r, g, b)
        }
        Message::ChangeColor(Color::Hsv(h, s, v)) => {
            println!("Change the color to hue {}, saturation {}, and value {}", h, s, v)
        }
        _ => (),
    }
}

match​ 第一个分支的模式匹配一个 Message::ChangeColor​ 枚举成员,该枚举成员又包含了一个 Color::Rgb​ 的枚举成员,最终绑定了 3 个内部的 i32​ 值。

第二个分支同理。

(4)解构结构体和元组

我们甚至可以用复杂的方式来混合、匹配和嵌套解构模式。如下是一个复杂结构体的例子,其中结构体和元组嵌套在元组中,并将所有的原始类型解构出来:

struct Point {
     x: i32,
     y: i32,
 }

let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });

这种将复杂类型分解匹配的方式,可以让我们单独得到感兴趣的某个值。

(5)解构数组

对于数组,我们可以用类似元组的方式解构,分为两种情况:

定长数组

let arr: [u16; 2] = [114, 514];
let [x, y] = arr;

assert_eq!(x, 114);
assert_eq!(y, 514);

不定长数组

let arr: &[u16] = &[114, 514];

if let [x, ..] = arr {
    assert_eq!(x, &114);
}

if let &[.., y] = arr {
    assert_eq!(y, 514);
}

let arr: &[u16] = &[];

assert!(matches!(arr, [..]));
assert!(!matches!(arr, [x, ..]));

6. 忽略模式中的值

有时忽略模式中的一些值是很有用的,比如在 match​ 中的最后一个分支使用 _​ 模式匹配所有剩余的值。 你也可以在另一个模式中使用 _​ 模式,使用一个以下划线开始的名称,或者使用 ..​ 忽略所剩部分的值。

(1)使用_​忽略整个值

虽然 _​ 模式作为 match​ 表达式最后的分支特别有用,但是它的作用还不限于此。例如可以将其用于函数参数中:

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {}", y);
}

fn main() {
    foo(3, 4);
}

这段代码会完全忽略作为第一个参数传递的值 3​,并会打印出 This code only uses the y parameter: 4​。

大部分情况当你不再需要特定函数参数时,最好修改签名不再包含无用的参数。在一些情况下忽略函数参数会变得特别有用,比如实现特征时,当你需要特定类型签名但是函数实现并不需要某个参数时。此时编译器就不会警告说存在未使用的函数参数,就跟使用命名参数一样。

(2)使用嵌套的_​忽略部分值

可以在一个模式内部使用 _​ 忽略部分值:

let mut setting_value = Some(5);
let new_setting_value = Some(10);

match (setting_value, new_setting_value) {
    (Some(_), Some(_)) => {
        println!("Can't overwrite an existing customized value");
    }
    _ => {
        setting_value = new_setting_value;
    }
}

println!("setting is {:?}", setting_value);

这段代码会打印出 Can't overwrite an existing customized value​ 接着是 setting is Some(5)​。

第一个匹配分支,我们不关心里面的值,只关心元组中两个元素的类型,因此对于 Some​ 中的值,直接进行忽略。 剩下的形如 (Some(_),None)​,(None, Some(_))​, (None,None)​ 形式,都由第二个分支 _​ 进行分配。

还可以在一个模式中的多处使用下划线来忽略特定值,如下所示,这里忽略了一个五元元组中的第二和第四个值:

let numbers = (2, 4, 8, 16, 32);

match numbers {
    (first, _, third, _, fifth) => {
        println!("Some numbers: {}, {}, {}", first, third, fifth)
    },
}

老生常谈:模式匹配一定要类型相同,因此匹配 numbers​ 元组的模式,也必须有五个值(元组中元素的数量也属于元组类型的一部分)。

这会打印出 Some numbers: 2, 8, 32​, 值 4 和 16 会被忽略。

(3)使用下划线开头​忽略未使用的变量

如果你创建了一个变量却不在任何地方使用它,Rust 通常会给你一个警告,因为这可能会是个 BUG。但是有时创建一个不会被使用的变量是有用的,比如你正在设计原型或刚刚开始一个项目。这时你希望告诉 Rust 不要警告未使用的变量,为此可以用下划线作为变量名的开头:

fn main() {
    let _x = 5;
    let y = 10;
}

这里得到了警告说未使用变量 y​,至于 x​ 则没有警告。

注意, 只使用 _​ 和使用以下划线开头的名称有些微妙的不同:比如_x​ 仍会将值绑定到变量,而 _​ 则完全不会绑定。

let s = Some(String::from("Hello!"));

if let Some(_s) = s {
    println!("found a string");
}

println!("{:?}", s);

s​ 是一个拥有所有权的动态字符串,在上面代码中,我们会得到一个错误,因为 s​ 的值会被转移给 _s​,在 println!​ 中再次使用 s​ 会报错:

error[E0382]: borrow of partially moved value: `s`
 --> src/main.rs:8:22
  |
4 |     if let Some(_s) = s {
  |                 -- value partially moved here
...
8 |     println!("{:?}", s);
  |                      ^ value borrowed here after partial move

只使用下划线本身,则并不会绑定值,因为 s​ 没有被移动进 _​:

let s = Some(String::from("Hello!"));

if let Some(_) = s {
    println!("found a string");
}

println!("{:?}", s);
(4)使用..​忽略剩余值

对于有多个部分的值,可以使用 ..​ 语法来只使用部分值而忽略其它值,这样也不用再为每一个被忽略的值都单独列出下划线。..​ 模式会忽略模式中剩余的任何没有显式匹配的值部分。

struct Point {
    x: i32,
    y: i32,
    z: i32,
}

let origin = Point { x: 0, y: 0, z: 0 };

match origin {
    Point { x, .. } => println!("x is {}", x),
}

这里列出了 x​ 值,接着使用了 ..​ 模式来忽略其它字段,这样的写法要比一一列出其它字段,然后用 _​ 忽略简洁的多。

还可以用 ..​ 来忽略元组中间的某些值:

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {}, {}", first, last);
        },
    }
}

这里用 first​ 和 last​ 来匹配第一个和最后一个值。..​ 将匹配并忽略中间的所有值。

然而使用 ..​ 必须是无歧义的。如果期望匹配和忽略的值是不明确的,Rust 会报错。下面代码展示了一个带有歧义的 ..​ 例子,因此不能编译:

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {}", second)
        },
    }
}

如果编译上面的例子,会得到下面的错误:

error: `..` can only be used once per tuple pattern // 每个元组模式只能使用一个 `..`
 --> src/main.rs:5:22
  |
5 |         (.., second, ..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here // 上一次使用在这里

error: could not compile `world_hello` due to previous error              ^^

Rust 无法判断,second​ 应该匹配 numbers​ 中的第几个元素,因此这里使用两个 ..​ 模式,是有很大歧义的!

7. 匹配守卫提供的额外条件

匹配守卫(match guard) 是一个位于 match​ 分支模式之后的额外 if​ 条件,它能为分支模式提供更进一步的匹配条件。

这个条件可以使用模式中创建的变量:

let num = Some(4);

match num {
    Some(x) if x < 5 => println!("less than five: {}", x),
    Some(x) => println!("{}", x),
    None => (),
}

这个例子会打印出 less than five: 4​。当 num​ 与模式中第一个分支匹配时,Some(4)​ 可以与 Some(x)​ 匹配,接着匹配守卫检查 x​ 值是否小于 5,因为 4 小于 5,所以第一个分支被选择。

相反如果 num​ 为 Some(10)​,因为 10 不小于 5 ,所以第一个分支的匹配守卫为假。接着 Rust 会前往第二个分支,因为这里没有匹配守卫所以会匹配任何 Some​ 成员。

模式中无法提供类如 if x < 5​ 的表达能力,我们可以通过匹配守卫的方式来实现。

在之前,我们提到可以使用匹配守卫来解决模式中变量覆盖的问题,那里 match​ 表达式的模式中新建了一个变量而不是使用 match​ 之外的同名变量。内部变量覆盖了外部变量,意味着此时不能够使用外部变量的值,下面代码展示了如何使用匹配守卫修复这个问题。

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {}", n),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {}", x, y);
}

现在这会打印出 Default case, x = Some(5)​。现在第二个匹配分支中的模式不会引入一个覆盖外部 y​ 的新变量 y​,这意味着可以在匹配守卫中使用外部的 y​。

相比指定会覆盖外部 y​ 的模式 Some(y)​,这里指定为 Some(n)​。此新建的变量 n​ 并没有覆盖任何值,因为 match​ 外部没有变量 n​。

匹配守卫 if n == y​ 并不是一个模式,所以没有引入新变量。这个 y​ 正是外部的 y​ 而不是新的覆盖变量 y​,这样就可以通过比较 n​ 和 y​ 来表达寻找一个与外部 y​ 相同的值的概念了。

也可以在匹配守卫中使用 运算符 |​ 来指定多个模式,同时匹配守卫的条件会作用于所有的模式。下面代码展示了匹配守卫与 |​ 的优先级。

这个例子中看起来好像 if y​ 只作用于 6​,但实际上匹配守卫 if y​ 是作用于 4​、5 6​ ,在满足 x​ 属于 4 | 5 | 6​ 后才会判断 y​ 是否为 true​:

let x = 4;
let y = false;

match x {
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
}

这个匹配条件表明此分支只匹配 x​ 值为 4​、5​ 或 6同时 y​ 为 true​ 的情况。

虽然在第一个分支中,x​ 匹配了模式 4​ ,但是对于匹配守卫 if y​ 来说,因为 y​ 是 false​,因此该守卫条件的值永远是 false​,也意味着第一个分支永远无法被匹配。

下面的文字图解释了匹配守卫作用于多个模式时的优先级规则,第一张是正确的:

(4 | 5 | 6) if y => ...

而第二张图是错误的

4 | 5 | (6 if y) => ...

可以通过运行代码时的情况看出这一点:如果匹配守卫只作用于由 |​ 运算符指定的值列表的最后一个值,这个分支就会匹配且程序会打印出 yes​。

8. @ 绑定

@​(读作 at)运算符允许为一个字段绑定另外一个变量。

下面例子中,我们希望测试 Message::Hello​ 的 id​ 字段是否位于 3..=7​ 范围内,同时也希望能将其值绑定到 id_variable​ 变量中以便此分支中相关的代码可以使用它。

我们可以将 id_variable​ 命名为 id​,与字段同名,不过出于示例的目的这里选择了不同的名称。

enum Message {
    Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    Message::Hello { id: id_variable @ 3..=7 } => {
        println!("Found an id in range: {}", id_variable)
    },
    Message::Hello { id: 10..=12 } => {
        println!("Found an id in another range")
    },
    Message::Hello { id } => {
        println!("Found some other id: {}", id)
    },
}

上例会打印出 Found an id in range: 5​。通过在 3..=7​ 之前指定 id_variable @​,我们捕获了任何匹配此范围的值并同时将该值绑定到变量 id_variable​ 上。

第二个分支只在模式中指定了一个范围,id​ 字段的值可以是 10、11 或 12​,不过这个模式的代码并不知情也不能使用 id​ 字段中的值,因为没有将 id​ 值保存进一个变量。

最后一个分支指定了一个没有范围的变量,此时确实拥有可以用于分支代码的变量 id​,因为这里使用了结构体字段简写语法。不过此分支中没有像头两个分支那样对 id​ 字段的值进行测试:任何值都会匹配此分支。

当你既想要限定分支范围,又想要使用分支的变量时,就可以用 @​ 来绑定到一个新的变量上,实现想要的功能。

@前绑定后解构(Rust 1.56 新增)

使用 @​ 还可以在绑定新变量的同时,对目标进行解构:

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {

    // 绑定新变量 `p`,同时对 `Point` 进行解构:
    // 使用 @ 符号将解构后的值绑定到 `p` 变量上
    // @ 符号右侧的 Point {x: px, y: py } 是一个模式(pattern),
    // 如果这个模式匹配就把匹配值绑定到 @ 符号左侧的 p 变量上。
  
	let p @ Point { x: px, y: py } = Point { x: 10, y: 23 };
    println!("x: {}, y: {}", px, py);
    println!("{:?}", p);

    let point = Point { x: 10, y: 5 };
  
	// 使用 if let 进行模式匹配,并绑定到 `p` 变量上
    if let p @ Point { x: 10, y } = point {
        println!("x is 10 and y is {} in {:?}", y, p);
    } else {
        println!("x was not 10 :(");
    }
}

// 运行结果:
// x: 10, y: 23
// Point { x: 10, y: 23 }
// x is 10 and y is 5 in Point { x: 10, y: 5 }
@新特性(Rust 1.53 新增)

考虑下面一段代码:

fn main() {
    match 1 {
        num @ 1 | 2 => {
            println!("{}", num);
        }
        _ => {}
    }
}

编译不通过,是因为 num​ 没有绑定到所有的模式上,只绑定了模式 1​,你可能会试图通过这个方式来解决:

num @ (1 | 2)

但是,如果你用的是 Rust 1.53 之前的版本,那这种写法会报错,因为编译器不支持。

至此,模式匹配的内容已经全部完结,复杂但是详尽,想要一次性全部记住属实不易,因此读者可以先留一个印象,等未来需要时,再来翻阅寻找具体的模式实现方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值