Skip to main content

Rust 基础入门 - 第五章

match 匹配

通用形式如下:


fn main() {
match target {
模式 1 => 表达式 1,
模式 2 | 模式 3 => {
语句 1;
语句 2;
表达式 2
},
_ => 表达式 3
}
}

其中,target 是要进行匹配的值,模式 是要匹配的模式,表达式 是要执行的代码。

例子:


fn main() {
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,
}
}
}

该函数 value_in_cents 接受一个 Coin 类型的参数,然后根据参数的值返回不同的美分数值。

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

模式绑定

模式匹配中可以从模式中取出绑定的值,例子如下:


#![allow(unused)]
fn main() {
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --skip--
}

enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState), // 25 美分硬币中包含州名
}
}

在枚举类型 Coin 中,Quarter 类型的值中包含一个 UsState 类型的值,我们可以从 Quarter 中取出 UsState 的值:

#![allow(unused)]
fn main() {
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => { // 从 Quarter 中取出 state
println!("State quarter from {:?}!", state);
25
},
}
}
}

如上代码中,在第八行,当匹配到 Coin::Quarter(state) 时,会将 state 绑定到 Coin::Quarter 中的 UsState 类型的值。

穷尽匹配

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");
},
};
}

这样严格的匹配,可以避免 null 陷阱。

_ 通配符

当不想列出所有的匹配情况时,可以使用 _ 通配符,如下代码:

#![allow(unused)]
fn main() {
let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}
}

_ 放置于所有匹配模式的最后,可以匹配遗漏的模式。在这个例子中,() 返回单元类型,即不做任何事。

if let 匹配

对于只有一种模式需要处理的场景,用 match 会显得有些冗余,这时可以使用 if let 来简化代码。

match 的通用形式如下:

#![allow(unused)]
fn main() {
let v = Some(3u8);
match v {
Some(3) => println!("three"),
_ => (),
}
}

这里最后的 _ 匹配多多少少有些冗余,可以使用 if let 来简化:

#![allow(unused)]
fn main() {
let v = Some(3u8);
if let Some(3) = v {
println!("three");
}
}

matches! 宏

在 Rust 标准库中,有一个很实用的宏 matches!,可以将一个表达式跟模式进行匹配,然后返回匹配的结果 true or false

比如我们想要判断一个字符是否是英文字母,可以用 match 这样写:

fn main() {
let foo = 'f';
println!("{}", get_foo(foo));
}

fn get_foo(c: char) -> bool {
match c {
'A'..='Z' => true,
'a'..='z' => true,
_ => false,
}
}

但是这种返回布尔值的情况,可以用 matches! 宏来简化:

fn main() {
let foo = 'f';
println!("{}", get_foo(foo));
}

fn get_foo(c: char) -> bool {
matches!(c, 'a'..='z' | 'A'..='Z')
}

Option 用法

Option 是一个枚举类型,它的定义如下:

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

简单来说,一个变量要么是 Some 类型,要么是 None 类型,Some 类型中包含一个值,None 类型中不包含任何值。

那么可以用 match 来判断 Option 类型的值:

#![allow(unused)]
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);
}

在这个模式匹配中,None 类型的值不会被处理,而 Some(5)Some(i) 属于相同的成员,并绑定值后加一。

note

因为 Option,Some,None 都包含在 prelude 中,因此你可以直接通过名称来使用它们,而无需以 Option::Some 这种形式去使用。

模式

说了这么多模式匹配,何为模式?

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

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

有如下用到模式的地方:

match 分支

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

if let 分支

if let PATTERN = SOME_VALUE {
/// do something
}

while let 循环

fn main() {
// 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);
}
}

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

for 循环

fn main() {
let v = vec!['a', 'b', 'c'];

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

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

let 语句

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

其实这里也是一种模式匹配,将一个元组解构成三个变量。

函数参数

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 得到了 3y 得到了 5

全模式列表

如下罗列所有模式语法:

匹配字面值

let x = 1;

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

这种最常用。

匹配命名变量

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

这个例子中,变量 x 不符合第一个分支,跳过,在第二个分支中,引入了一个新的变量 y,但是它在 match 的作用域中,和上面的那个 y 不是同一个变量,因此这个新绑定的 y 的值是 5,而不是 10

单分支多模式

可以用 | 来匹配多个模式,代表或的关系:

let x = 1;

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

序列匹配范围

..= 表示闭区间,.. 表示开区间,可以用来匹配序列中的值,当模式匹配任何在此序列内的值时,该分支会执行:

let x = 5;

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

这里的序列只能用于数字或者字符类型,因为这两种类型都是可以连续的。同时编译器在编译期可以检查该序列是否为空。

解构分解值

也用模式来解构结构体、元组、枚举、数组和引用:

解构结构体

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

这里创建了一个结构体 Point,然后创建了一个 p 的实例,然后使用模式匹配来解构 p,将 xy 的值分别赋值给 ab

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

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

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

这个例子中,值 p 因为其 x 包含 0 而匹配第二个分支。

解构枚举

enum Message {
Quit,
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
)
}
}
}

模式匹配要类型相同。

解构嵌套的结构体和枚举

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::Hsv 的枚举成员,最终绑定了 3 个内部的 i32 值。

解构数组

和元组类似:

固定长度数组
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, ..]));
caution

这个还不懂。可恶

忽略模式中的值

使用下划线

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

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

使用嵌套的下划线忽略部分值

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);

这个例子中,第一个匹配分支,只关心 setting_valuenew_setting_value 是否都是 Some 值,所以使用了下划线忽略了它们的值。

使用下划线忽略未使用的变量

这个之前提到过,就是为了消除编译器的警告。

但是,只使用 和使用以下划线开头的名称有些微妙的不同:比如 _x 仍会 将值绑定到变量 ,而 `` 则 完全不会绑定 。比如下面两个例子:

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

if let Some(_s) = s { // 这里的 _s 仍然会绑定到 s 的值,s 的值会被移动到 _s 中
println!("found a string");
}

println!("{:?}", s); // s 的值已经被移动到 _s 中,所以这里报错
}

在上面代码中,我们会得到一个错误,因为 s 的值会被转移给 _s。但是,如果我们使用 _ 而不是 _s,则不会发生这种情况:

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

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

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

使用.. 忽略剩余的值

对于有多个部分的值,可以使用 .. 语法来只使用部分值而忽略其它值,这样也不用再为每一个被忽略的值都单独列出下划线。

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

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

主打一个优雅。但是同时要主要的是,.. 不能存在歧义,比如下面这个例子:

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

match numbers {
(.., second, ..) => {
println!("Some numbers: {}", second) // only God knows which number this is
},
}
}

匹配守卫

匹配守卫,(match guards) 允许我们在匹配模式的同时,加入额外的条件判断。如果这个条件判断为 true,则执行匹配分支,否则继续尝试下一个匹配分支。

let num = Some(4);

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

在这个例子中,当 num 的值为 Some(4) 时,匹配守卫 x < 5true,所以匹配分支会被执行。

当一个匹配分支中,指定了多个模式,用 | 分隔,这时,匹配守卫的优先级是独立的,比如下面这个例子:

let x = 4;
let y = false;

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

优先级是这样的:

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

@ 绑定

@ 绑定,(at binding) 允许我们将匹配的值绑定到一个变量,这个变量可以在匹配分支中使用。

fn main() {
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)
},
}
}

在这个例子中,第一个分支中,测试 id 的值是否在 3..=7 范围内,如果是,则将 id 的值绑定到 id_variable 中,然后在匹配分支中打印出 id_variable

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

参考资料