Rust第十六节 - 模式匹配

165 阅读9分钟

16 模式匹配

模式是Rust中一种用来匹配类型结构的特殊语法,它时而复杂,时而简单。

一个模式通常由以下组件组合而成:

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

我们会在这一节讨论所有可以使用模式匹配的场景不可失败模式可失败模式之间的区别,以及代码中可能会出现的各种模式匹配语法。

16.1 所有可以使用模式的场合

16.1.1 match分支

match表达式在形式上由match关键字、待匹配的值,以及至少一个匹配分支组合而成。

match 值 { 
    模式 => 表达式,
    模式 => 表达式,
    模式 => 表达式,
}

使用match匹配,我们需要穷尽所有的可能性,所有我们之前会使用到_来匹配所有的值,它表示所有未被指定的值。

16.1.2 if let 表达式

我们在之前的章节中将if let当作只匹配单个分支的match表达式来使用。但实际上还有elseelse ifelse if let表达式来进行匹配。如下:

let num: Result<i32, _> = "1".parse();
if let Some(num) = Some('1') {
    println!("if let")
} else if true {
    println!("else if")
} else if let Ok(num) = num {
    println!("else if let")
} else {
    println!("else")
}

我们可以自行调整值,来让不同的模式匹配执行。

match表达式不同,if let表达式的不利之处在于它不会强制开发者穷尽值的所有可能性

16.1.3 while let条件循环

条件循环while let的构造与if let十分类似,但它会反复执行同一个模式匹配直到出现失败的情形

let mut arr = vec![1, 2, 3];

while let Some(num) = arr.pop() {
    println!("{}", num)
}
// 3
// 2
// 1

只要arr.pop()返回的是Some变体,就会一直执行下去,直到变为None

16.1.4 for循环

for循环是Rust代码中最为常用的循环结构:

let arr = vec!['a', 'b', 'b'];

for (index, value) in arr.iter().enumerate() {
    println!("{}的索引是{}", value, index)
}
// a的索引是0
// b的索引是1
// b的索引是2

上面的代码使用了enumerate方法来作为迭代器的适配器,它会在每次迭代过程中生成一个包含值本身及值索引的元组。

16.1.5 let语句

Rust中最简单的匹配模式就是let

let x = 5;

它表示的意思就是将此处匹配到的所有内容绑定至变量x,因为x就是整个模式本身,所以它实际上意味着“无论表达式会返回什么样的值,我们都可以将它绑定至变量x中。 我们再来看个例子:

let (x, y) = (1, 2, 3);
println!("{:?}", x)

如果模式中元素的数量与元组中元素的数量不同,那么整个类型就会匹配失败,进而导致编译错误。如果你需要忽略元组中的某一个或多个值,那么我们可以使用_或..语法

let (x, y, _) = (1, 2, 3);
println!("{:?}", x)

然后就可以正常执行了。

16.1.6 函数的参数

函数的参数同样也是模式,签名中的x部分就是一个模式:

fn foo(x: i32) {
   // 在此编写函数代码
}

16.2 可失败性:模式是否会匹配失败

模式可以被分为**不可失败(irrefutable)可失败(refutable)**两种类型。

  • 函数参数let语句for循环只接收不可失败模式,因为在这些场合下,我们的程序无法在值不匹配时执行任何有意义的行为。
  • if letwhile let表达式则只接收可失败模式,因为它们在被设计时就将匹配失败的情形考虑在内了:条件表达式的功能就是根据条件的成功与否执行不同的操作。

假如我们试图在需要不可失败模式的场合中使用可失败模式会发生些什么呢?

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

我们通过上面的方式给代码添加了一个合法的出口!你可以顺利地运行这段代码,尽管这意味着我们必须在此时使用可失败模式。假如我们在if let中使用了一个不可失败模式,那么这段代码是无法通过编译的

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

因此,在match表达式的匹配分支中,除了最后一个,其他必须全部使用可失败模式,而最后的分支则应该使用不可失败模式,因为它需要匹配值的所有剩余的情形。

16.3 模式语法

本节会整理所有的关于模式的语法:)

16.3.1 匹配字面量

最简单的一个匹配就是匹配字面量:

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

16.3.2 匹配命名变量

命名变量是一种可以匹配任何值的不可失败模式。由于match开启了一个新的作用域,所以被定义在match表达式内作为模式一部分的变量会覆盖掉match结构外的同名变量,正如覆盖其他普通变量一样。

let x = Some(5);
let y = 6;

match x {
    Some(y) => {
        println!("{}", y);
    }
    _ => {
        println!("匹配到其他值")
    }
}

println!("{}", y);

这里因为因为我们的Some变体中的参数是y,所以打印出来的y不会使用外部的y值,而是使用变体中的值。

16.3.3 多重模式

你可以在match表达式的分支匹配中使用|来表示或(or)的意思

let x = 1;
match x {
    1 | 2 => {
        println!("匹配成功")
    }
    _ => {
        println!("匹配失败")
    }
}

16.3.4 使用...来匹配值区间

我们可以使用...来匹配闭区间的值。

let x = 5;
match x {
    1..=5 => {
        println!("成功匹配1-5之间的值")
    }
    _ => {
        println!("匹配失败")
    }
}

当然也可以匹配字符串的值:

let x = 'a';
match x {
    'a'..='c' => {
        println!("成功匹配a-c之间的值")
    }
    _ => {
        println!("匹配失败")
    }
}

16.3.5 使用解构来分解值

我们可以使用模式来分解结构体、枚举、元组或引用,从而使用这些值中的不同部分

16.3.5.1 解构结构体

我们可以对结构体解构并可以重新命名,如下:

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

let pt = Point { x: 1, y: 2 };
let Point { x, y } = pt;
let Point { x: a, y: b } = pt;

println!("x={}, y={}", x, y);
println!("a={}, b={}", a, b);

我们还可以使用模式来匹配解构的值,如下:

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

let pt = Point { x: 1, y: 2 };

match pt {
    Point { x, y: 0 } => {
        println!("匹配X轴上面的点={}", x)
    }
    Point { x: 0, y } => {
        println!("匹配Y轴上面的点={}", y)
    }
    Point { x, y } => {
        println!("x={},y={}", x, y);
    }
}

我们声明的Point点位信息,然后我们可以通过模式去匹配X,Y轴上面的点。

16.3.5.2 解构枚举

我们解构枚举的时候需要注意:用于解构枚举的模式必须要对应枚举定义中存储。

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

let x = Message::ChangeColor(255, 255, 255);
match x {
    Message::Quit => {
        println!("退出")
    }
    Message::Move { x, y } => {
        println!("移动到: x={}, y={}", x, y);
    }
    Message::Write(s) => {
        println!("写下了:{}", &s[0..])
    }
    Message::ChangeColor(a, b, c) => {
        println!("色值:a={}, b={}, c={}", a, b, c);
    }
}

注意:模式中的变量数目必须与目标变体中的元素数目完全一致,否则会出现编译错误。

16.3.5.3 解构嵌套的结构体和枚举

到目前为止,我们所有的示例都只匹配了单层的结构体或枚举,但匹配语法还可以被用于嵌套的结构中。 我们改造一下上面的例子,再声明一个元祖结构体,将它嵌套在第一个结构体中的ChangeColor变体中:

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

let x = Message::ChangeColor(Color(255, 255, 255));
match x {
    Message::Quit => {
        println!("退出")
    }
    Message::Move { x, y } => {
        println!("移动到: x={}, y={}", x, y);
    }
    Message::Write(s) => {
        println!("写下了:{}", &s[0..])
    }
    Message::ChangeColor(Color(a, b, c)) => {
        println!("色值:a={}, b={}, c={}", a, b, c);
    }
}
16.3.5.4 解构结构体和元组

我们甚至可以按照某种更为复杂的方式来将模式混合、匹配或嵌套在一起

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

这段代码能够将复杂的类型值分解为不同的组成部分,以便使我们可以分别使用自己感兴趣的值。

16.3.5 忽略模式中的值

某些场景下忽略模式中的值是有意义的,例如在match表达式的最后一个分支中,代码可以匹配剩余所有可能的值而又不需要执行什么操作。有几种不同的方法可以让我们在模式中忽略全部或部分值:

  • 使用_模式
  • 在另一个模式中使用_模式
  • 使用以下画线开头的名称
  • 或者使用..来忽略值的剩余部分。
16.3.5.1 使用_忽略整个值

我么可以将下画线_作为通配符模式来匹配任意可能的值而不绑定值本身的内容,例如:

fn foo(_: i32, y: i32) {
    println!("{}", y);
}
16.3.5.2 使用嵌套的_忽略值的某些部分

我们还可以使用_忽略值的某一部分,比如下面,如果我们

let mut setting_value = Some(1);
let new_setting_value = Some(2);

match (setting_value, new_setting_value) {
    (Some(_), Some(_)) => {
        println!("不会覆盖值");
    }
    _ => {
        setting_value = new_setting_value;
    }
}
println!("{:?}", setting_value);

我们也可以在一个模式中多次使用下画线来忽略多个特定的值,如:

let numbers = (1, 2, 3, 4, 5);
match numbers {
    (a, _, b, _, c) => {
        println!("a={}, b={}, c={}", a, b, c)
    }
    _ => {
        println!("未匹配")
    }
}

为了避免Rust在这些场景中因为某些未使用的变量而抛出警告,我们可以在这些变量的名称前添加下画线,例如:

let x = 1;
let _y = 2;
println!("x={}", x);
16.3.5.3 使用..忽略值的剩余部分

对于拥有多个部分的值,我们可以使用..语法来使用其中的某一部分并忽略剩余的那些部分。这使我们不必为每一个需要忽略的值都添加对应的_模式来进行占位。..模式可以忽略一个值中没有被我们显式匹配的那些部分

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

let pt = Point { x: 1, y: 2, z: 3 };
match pt {
    Point { x, .. } => {
        println!("x={}", x);
    }
}

我们再来试试在元祖里面使用,可以将第一位和最后一位匹配出来:

let tp = (1, 2, 3, 4, 5, 6);
match tp {
    (first, .., last) => {
        println!("first={}, last={}", first, last)
    }
}

16.3.6 使用匹配守卫添加额外条件

匹配守卫(match guard)是附加在match分支模式后的if条件语句,分支中的模式只有在该条件被同时满足时才能匹配成功。相比于单独使用模式,匹配守卫可以表达出更为复杂的意图。

let x: Option<i32> = Some(5);

match x {
    Some(y) if y < 6 => {
        println!("匹配成功")
    }
    _ => {
        println!("匹配失败")
    }
}

16.3.7 @绑定

@运算符允许我们在测试一个值是否匹配模式的同时创建存储该值的变量

struct Message {
    id: i32,
}

let msg = Message { id: 3 };
match msg {
    Message { id: sub_id @ 1..=7 } => {
        println!("{}", sub_id)
    }
    Message { id } => {
        println!("{}", id)
    }
}