Rust-模式匹配

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

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

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

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

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

一、match 和 if let

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

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

rust 复制代码
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​ 的通用形式:

rust 复制代码
match target {
    模式1 => 表达式1,
    模式2 => {
        语句1;
        语句2;
        表达式2
    },
    _ => 表达式3
}

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

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

rust 复制代码
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​ 本身也是一个表达式,因此可以用它来赋值:

rust 复制代码
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)模式绑定

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

rust 复制代码
enum UsState {
    Alabama,
    Alaska,
    // ...48个其他州的名字
}

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

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

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

rust 复制代码
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​ 的枚举值。

就像下面这样:

rust 复制代码
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

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

rust 复制代码
/// 枚举类型,表示一个动作
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,
                );
            }
        }
    }
}

运行后输出:

console 复制代码
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​ 的匹配必须穷尽所有情况,下面来举例说明,例如:

rust 复制代码
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​ 的值。那么, 我们不必一个一个列出所有值, 因为可以使用特殊的模式 _​ 替代:

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

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

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

rust 复制代码
#[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​ 来处理就要写成下面这样:

rust 复制代码
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​ 的方式来实现:

rust 复制代码
if let Some(3) = v {
    println!("three");
}

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

3. matches! 宏

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

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

rust 复制代码
enum MyEnum {
    Foo,
    Bar
}

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

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

rust 复制代码
v.iter().filter(|x| x == MyEnum::Foo);

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

rust 复制代码
v.iter().filter(|x| matches!(x, MyEnum::Foo));

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

rust 复制代码
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​,这里都是一个新的代码块,而且这里的绑定相当于新变量,如果你使用同名变量,会发生变量遮蔽:

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

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

运行后输出如下:

console 复制代码
在匹配前,age是Some(30)
匹配出来的age是30
在匹配后,age是Some(30)

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

对于 match​ 类型也是如此:

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

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

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

二、解构 Option

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

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​ 值:

rust 复制代码
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)
rust 复制代码
None => None,

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

rust 复制代码
Some(i) => Some(i + 1),

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

传入参数 None

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

rust 复制代码
None => None,

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

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

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

1. match 分支

rust 复制代码
match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

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

rust 复制代码
match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    _ => EXPRESSION,
}

2. if let 分支

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

rust 复制代码
if let PATTERN = SOME_VALUE {

}

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

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

rust 复制代码
if let PATTERN = SOME_VALUE {
    //表达式...
} else {
    //表达式...
}

3. while let 条件循环

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

rust 复制代码
// 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​。

rust 复制代码
let v = vec!['a', 'b', 'c'];

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

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

5. let 语句

rust 复制代码
let PATTERN = EXPRESSION;

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

rust 复制代码
let x = 5;

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

rust 复制代码
let (x, y, z) = (1, 2, 3);

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

6. 函数参数

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

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

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

rust 复制代码
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

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

rust 复制代码
let Some(x) = some_option_value;

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

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

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

rust 复制代码
if let Some(x) = some_option_value {
    println!("{}", x);
}

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

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

1. 匹配字面值

rust 复制代码
let x = 1;

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

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

2. 匹配命名变量

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

rust 复制代码
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​ 的值匹配此分支的任何一个模式,它就会运行:

rust 复制代码
let x = 1;

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

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

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

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

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

rust 复制代码
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 中仅有的可以用于判断是否为空的类型。

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

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​:

rust 复制代码
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​:

rust 复制代码
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​ 中的值。

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

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

rust 复制代码
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​ 使用模式解构每一个内部值:

rust 复制代码
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 色彩模式:

rust 复制代码
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)解构结构体和元组

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

rust 复制代码
struct Point {
     x: i32,
     y: i32,
 }

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

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

(5)解构数组

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

定长数组

rust 复制代码
let arr: [u16; 2] = [114, 514];
let [x, y] = arr;

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

不定长数组

rust 复制代码
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​ 表达式最后的分支特别有用,但是它的作用还不限于此。例如可以将其用于函数参数中:

rust 复制代码
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)使用嵌套的_​忽略部分值

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

rust 复制代码
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)​ 形式,都由第二个分支 _​ 进行分配。

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

rust 复制代码
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 不要警告未使用的变量,为此可以用下划线作为变量名的开头:

rust 复制代码
fn main() {
    let _x = 5;
    let y = 10;
}

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

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

rust 复制代码
let s = Some(String::from("Hello!"));

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

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

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

rust 复制代码
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​ 没有被移动进 _​:

rust 复制代码
let s = Some(String::from("Hello!"));

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

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

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

rust 复制代码
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​ 值,接着使用了 ..​ 模式来忽略其它字段,这样的写法要比一一列出其它字段,然后用 _​ 忽略简洁的多。

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

rust 复制代码
fn main() {
    let numbers = (2, 4, 8, 16, 32);

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

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

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

rust 复制代码
fn main() {
    let numbers = (2, 4, 8, 16, 32);

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

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

rust 复制代码
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​ 条件,它能为分支模式提供更进一步的匹配条件。

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

rust 复制代码
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​ 之外的同名变量。内部变量覆盖了外部变量,意味着此时不能够使用外部变量的值,下面代码展示了如何使用匹配守卫修复这个问题。

rust 复制代码
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​:

rust 复制代码
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​,也意味着第一个分支永远无法被匹配。

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

text 复制代码
(4 | 5 | 6) if y => ...

而第二张图是错误的

text 复制代码
4 | 5 | (6 if y) => ...

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

8. @ 绑定

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

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

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

rust 复制代码
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 新增)

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

rust 复制代码
#[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 新增)

考虑下面一段代码:

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

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

rust 复制代码
num @ (1 | 2)

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

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

相关推荐
跟着珅聪学java1 小时前
spring boot +Elment UI 上传文件教程
java·spring boot·后端·ui·elementui·vue
我命由我123451 小时前
Spring Boot 自定义日志打印(日志级别、logback-spring.xml 文件、自定义日志打印解读)
java·开发语言·jvm·spring boot·spring·java-ee·logback
徐小黑ACG2 小时前
GO语言 使用protobuf
开发语言·后端·golang·protobuf
0白露3 小时前
Apifox Helper 与 Swagger3 区别
开发语言
Tanecious.4 小时前
机器视觉--python基础语法
开发语言·python
叠叠乐4 小时前
rust Send Sync 以及对象安全和对象不安全
开发语言·安全·rust
战族狼魂5 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
niandb5 小时前
The Rust Programming Language 学习 (九)
windows·rust
Tttian6226 小时前
Python办公自动化(3)对Excel的操作
开发语言·python·excel
杉之6 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue