Rust中经常使用到的一个功能是模式匹配,例如let变量赋值本质上就是模式匹配。
官方手册参考:https://doc.rust-lang.org/reference/patterns.html。
可在如下几种情况下使用模式匹配:
let变量赋值时的模式匹配:
let PATTERN = EXPRESSION;
变量是一种最简单的模式,变量名位于Patter位置,赋值时的过程:「将表达式与模式进行比较匹配,并将任何找到的变量名进行赋值」。
例如:
let x = 5;
let (x,y) = (1,2);
第一条语句,变量x是一个模式,在执行该语句时,将表达式5赋值给找到的变量名x。变量赋值总是可以匹配成功。
第二条语句,将表达式(1,2)
和模式(x,y)
进行匹配,匹配成功,于是为找到的变量x和y进行赋值:x=1,y=2
。
如果模式中的元素数量和表达式中返回的元素数量,则匹配失败,编译将无法通过。
let (x,y,z) = (1,2); // 失败
为函数参数传值和使用let变量赋值是类似的,本质都是在做模式匹配的操作。
例如:
fn f1(i: i32){
// xxx
}
fn f2(&(x,y): &(i32,i32)){
// yyy
}
函数f1
的参数i
就是模式,当调用f1(88)
时,88是表达式,将赋值给找到的变量名i。
函数f2
的参数&(x,y)
是模式,调用f2( &(2,8) )
时,将表达式&(2,8)
与模式&(x,y)
进行匹配,并为找到的变量名x和y进行赋值:x=2,y=8
。
match分支匹配的用法非常灵活。它的语法为:
match VALUE {
PATTERN1 => EXPRESSION1,
PATTERN2 => EXPRESSION2,
PATTERN3 => EXPRESSION3,
}
例如,可以使用match来穷举枚举类型的所有成员:
enum Device {
Laptop,
Desktop,
Phone,
Pad,
}
fn main(){
match Device::Desktop {
Device::Laptop => 1,
Device::Desktop => 2,
Device::Phone => 3,
_ => 4,
}
}
使用match时,要求穷尽所有可能的情况,如果有遗漏的情况,编译将失败。
可以将_
作为最后一个分支的PATTERN,它将匹配剩余所有情况。正如上面的示例。
另外,match自身也是表达式,它可以赋值给某个变量。
let x = match Device::Desktop {
Device::Laptop => 1,
Device::Desktop => 2,
Device::Phone => 3,
_ => 4,
};
if let
是match的一种特殊情况的语法糖:当只关心一个match分支,其余情况全部由_
负责匹配时,可以将其改写为更精简if let
语法。
if let PATTERN = EXPRESSION {
// xxx
}
这表示将EXPRESSION的返回值与PATTERN模式进行匹配,如果匹配成功,则为找到的变量进行赋值,这些变量在大括号作用域内有效。如果匹配失败,则不执行大括号中的代码。
例如:
let u8_value = Some(5_u8);
if let Some(5) = u8_value { // 匹配了但是没有找到变量
println!("five");
}
// 等价于如下代码
let u8_value = Some(5_u8);
match u8_value {
Some(5) => println!("five"),
_ => (),
}
if let
可以结合else if
、else if let
和else
一起使用。
if let PATTERN = EXPRESSION {
// XXX
} else if {
// YYY
} else if let PATTERN = EXPRESSION {
// zzz
} else {
// zzzzz
}
这时候它们和match多分支类似。但实际上有很大的不同,使用match分支匹配时,要求分支之间是关联的(例如枚举的各个成员)且穷尽的,但Rust编译器不会检查if let
的模式之间是否有关联关系,也不检查if let
是否穷尽所有可能情况,因此,即使在逻辑上有错误,Rust也不会给出编译错误提醒。
例如,《The Rust Programming Language》给出了一个示例:
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
if let Some(color) = favorite_color {
println!("Using your favorite color, {}, as the background", color);
} else if is_tuesday {
println!("Tuesday is green day!");
} else if let Ok(age) = age { // 注意,age只在这个分支大括号内有效
if age > 30 {
println!("Using purple as the background color");
} else {
println!("Using orange as the background color");
}
} else {
println!("Using blue as the background color");
}
}
只要while let
的模式匹配成功,就会一直执行while循环内的代码。
例如:
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top);
}
当stack.pop
成功时,将匹配Some(top)
成功,并将pop的值赋值给top,当没有元素可pop时,返回None,匹配失败,于是while循环退出。
这个无需解释。一个示例即可:
let v = vec!['a','b','c'];
for (idx, value) in v.iter().enumerate(){
println!("{}: {}", idx, value);
}
从前面介绍的几种模式匹配可了解到,模式匹配的方式不唯一,有的时候是一定匹配成功的变量赋值型(let/for/函数传参)模式匹配,有的时候是可能匹配失败的模式匹配。
Rust中为这两种定义了专门的称呼:
「let变量赋值、for循环、函数传参」这三种模式匹配只接受不可反驳模式。「if let和while let」只接受可反驳模式。
「match」支持两种模式:
_
作为最后一个分支时,是不可反驳模式,它一定会匹配成功当模式匹配处使用了不接受的模式时,将会编译错误或给出警告。
// let变量赋值时使用可反驳的模式(允许匹配失败),编译失败
let Some(x) = some_value;
// if let处使用了不可反驳模式,没有意义(一定会匹配成功),给出警告
if let x = 5 {
// xxx
}
对于match来说,有以下几个示例可说明它的使用方式:
match value {
Some(5) => (), // 允许匹配失败,是可反驳模式
Some(50) => (),
_ => (), // 一定会匹配成功,是不可反驳模式
}
match value {
x => println!("{}", x), // 当只有一个Pattern分支时,可以是不可反驳模式
_ => (),
}
模式部分可以是字面量:
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
_ => println!("anything"),
}
例如:
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {:?}", y), // 匹配成功,输出5
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {:?}", x, y); // 输出10
}
上面的match会匹配第二个分支,同时为找到的变量y进行赋值,即y=5
。这个y只在第二个分支对应的代码部分有效,跳出作用域后,y恢复为y=10
。
使用|
可组合多个模式,表示逻辑或(or)的意思。
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
Rust支持数值和字符的范围,有如下几种范围表达式:
Production | Syntax | Range |
---|---|---|
RangeExpr | start..end | start ≤ x < end |
RangeFromExpr | start.. | start ≤ x |
RangeToExpr | ..end | x < end |
RangeFullExpr | .. | - |
RangeInclusiveExpr | start..=end | start ≤ x ≤ end |
RangeToInclusiveExpr | ..=end | x ≤ end |
但范围作为模式时,只允许全闭合的..=
范围,其他类型的范围都会报错。
例如:
// 数值范围
let x = 79;
match x {
0..=59 => println!("不及格"),
60..=89 => println!("良好"),
90..=100 => println!("优秀"),
_ => println!("error"),
}
// 字符范围
let y = 'c';
match y {
'a'..='j' => println!("a..j"),
'k'..='z' => println!("k..z"),
_ => (),
}
模式可用于解构赋值,可解构的类型包括struct、enum、tuple以及它们的引用。
解构赋值时,可使用_
作为某个变量的占位符,使用..
作为剩余所有变量的占位符(使用..
时不能产生歧义,例如(..,x,..)
是有歧义的)。当解构的类型包含了命名字段时,可使用filedname
简化fieldname: fieldname
的书写。
struct Point2 {
x: i32,
y: i32,
}
struct Point3 {
x: i32,
y: i32,
z: i32,
}
fn main(){
let p = Point2{x: 0, y: 7};
// 等价于 let Point2{x: x, y: y} = p;
let Point2{x, y} = p;
// 解构时可修改变量名: let Point2{x: a, y: b} = p;
println!("x: {}, y: {}", x, y);
let ori = Point{x: 0, y: 0, z: 0};
match origin{
// 使用..忽略解构后剩余的值
Point3 {x, ..} => println!("{}", x),
}
}
enum IPAddr {
IPAddr4(u8,u8,u8,u8),
IPAddr6(String),
}
fn main(){
let ipv4 = IPAddr::IPAddr4(127,0,0,1);
match ipv4 {
// 丢弃解构后的第四个值
IPAddr(a,b,c,_) => println!("{},{},{}", a,b,c),
IPAddr(s) => println!("{}", s),
}
}
let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });
当解构后进行模式匹配时,如果某个值没有对应的变量名,则可以使用@
手动绑定一个变量名。
例如:
struct S(i32, i32);
match S(1, 2) {
S(z @ 1, _) | S(_, z @ 2) => assert_eq!(z, 1),
_ => panic!(),
}
再例如:
match slice {
[.., "!"] => println!("!!!"),
[start @ .., "z"] => println!("starts with: {:?}", start),
["a", end @ ..] => println!("ends with: {:?}", end),
rest => println!("{:?}", rest),
}
当进行解构赋值时,很可能会将变量拥有的所有权转移出去,从而使得原始变量变得不完整或直接失效。
struct Person{
name: String,
age: i32,
}
fn main(){
let p = Person{name: String::from("junmajinlong"), age: 23};
let Person{name, age} = p;
println!("{}", name);
println!("{}", age);
println!("{}", p.name); // 错误,name字段所有权已转移
}
如果想要在解构赋值时不丢失所有权,有以下几种方式:
// 方式一:解构表达式的引用
let Person{name, age} = &p;
// 方式二:解构表达式的克隆,适用于可调用clone()方法的类型
// 但Person struct没有clone()方法
// 方式三:在模式部分使用ref关键字修改变量
let Person{ref name, age} = p;
let Person{name: ref n, age} = p;
在模式中使用ref
修改变量名相当于在被解构值上加&
符号表示引用。
let x = 5_i32; // x的类型:i32
let x = &5_i32; // x的类型:&i32
let ref x = 5_i32; // x的类型:&i32
let ref x = &5_i32; // x的类型:&&i32
因此,使用ref修饰了模式中的变量名后,对应值的所有权就不会发生转移,而是只读借用给该变量。
如果想要对解构赋值的变量具有数据的修改权,需要使用mut关键字修饰模式中的变量,但这样会转移原值的所有权,此时可不要求原变量是可变的。
#[derive(Debug)]
struct Person {
name: String,
age: i32,
}
fn main() {
let p = Person {
name: String::from("junma"),
age: 23,
};
match p {
Person { mut name, age } => {
name.push_str("jinlong");
println!("name: {}, age: {}", name, age)
},
}
//println!("{:?}", p); // 错误
}
如果不想在可修改数据时丢失所有权,可在mut的基础上加上ref关键字,就像&mut xxx
一样。
#[derive(Debug)]
struct Person {
name: String,
age: i32,
}
fn main() {
let mut p = Person { // 这里要改为mut p
name: String::from("junma"),
age: 23,
};
match p {
// 这里要改为ref mut name
Person { ref mut name, age } => {
name.push_str("jinlong");
println!("name: {}, age: {}", name, age)
},
}
println!("{:?}", p);
}
最后,也可以将match value{}
的value进行修饰,例如match &mut value {}
,这样就不需要在模式中去加ref和mut了。这对于有多个分支需要解构赋值,且每个模式中都需要ref/mut修饰变量的match非常有用。
fn main() {
let mut x : Option<String> = Some("hello".into());
match &mut x { // 在这里borrow
Some(i) => i.push_str("world"), // 这里的i就不用再borrow
None => println!("None"),
}
println!("{:?}", x);
}
匹配守卫允许匹配分支添加「额外的后置」条件:当匹配了某分支的模式后,再检查该分支的守卫后置条件,如果守卫条件也通过,则成功匹配该分支。
let num = Some(4);
match num {
// 匹配Some(x)后,再检查x是否小于5
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
注意,后置条件的优先级很低。例如:
// 下面两个分支的写法等价
4 | 5 | 6 if y => println!("yes"),
(4 | 5 | 6) if y => println!("yes"),