match 和 if let
在 Rust 中,模式匹配最常用的就是 match
和
if let
。
先来看一个关于 match
的简单例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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
的通用形式:
1 2 3 4 5 6 7 8 9 match target { 模式1 => 表达式1 , 模式2 => { 语句1 ; 语句2 ; 表达式2 }, _ => 表达式3 }
该形式清晰的说明了何为模式,何为模式匹配:将模式与
target
进行匹配,即为模式匹配,而模式匹配不仅仅局限于
match
,后面我们会详细阐述。
match
允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行对应的代码,下面让我们来一一详解,先看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
函数根据匹配到的硬币,返回对应的美分数值。match
后紧跟着的是一个表达式,跟 if
很像,但是 if
后的表达式必须是一个布尔值,而 match
后的表达式返回值可以是任意类型,只要能跟后面的分支中的模式匹配起来即可,这里的
coin
是枚举 Coin
类型。
接下来是 match
的分支。一个分支有两个部分:一个模式和针对该模式的处理代码 。第一个分支的模式是
Coin::Penny
,其后的 =>
运算符将模式和将要运行的代码分开。这里的代码就仅仅是表达式
1
,不同分支之间使用逗号分隔。
当 match
表达式执行时,它将目标值 coin
按顺序依次与每一个分支的模式相比较,如果模式匹配了这个值,那么模式之后的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支。
每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个
match
表达式的返回值。如果分支有多行代码,那么需要用
{}
包裹,同时最后一行代码需要是一个表达式。
1.1 使用 match
表达式赋值
还有一点很重要,match
本身也是一个表达式,因此可以用它来赋值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
。
1.2 模式绑定
模式匹配的另外一个重要功能是从模式中取出绑定的值,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 #[derive(Debug)] enum UsState { Alabama, Alaska, }enum Coin { Penny, Nickel, Dime, Quarter (UsState), }
其中 Coin::Quarter
成员还存放了一个值:美国的某个州(因为在 1999 年到 2008 年间,美国在 25
美分(Quarter)硬币的背后为 50
个州印刷了不同的标记,其它硬币都没有这样的设计)。
接下来,我们希望在模式匹配中,获取到 25
美分硬币上刻印的州的名称:
1 2 3 4 5 6 7 8 9 10 11 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
的枚举值。
再来看一个更复杂的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 enum Action { Say (String ), MoveTo (i32 , i32 ), ChangeColorRGB (u16 , u16 , u16 ), }fn main () { let actions = [ Action::Say ("Hello Rust" .to_string ()), Action::MoveTo (1 ,2 ), Action::ChangeColorRGB (255 ,255 ,0 ), ]; for action in actions { match action { Action::Say (s) => { println! ("{}" , s); }, Action::MoveTo (x, y) => { println! ("point from (0, 0) move to ({}, {})" , x, y); }, Action::ChangeColorRGB (r, g, _) => { println! ("change color into '(r:{}, g:{}, b:0)', 'b' has been ignored" , r, g, ); } } } }
运行后输出:
1 2 3 4 5 6 7 $ cargo run Compiling world_hello v0.1.0 (/Users/sunfei/development/rust/world_hello) Finished dev [unoptimized + debuginfo] target(s) in 0.16s Running `target/debug/world_hello` Hello Rust point from (0, 0) move to (1, 2) change color into '(r:255, g:255, b:0)', 'b' has been ignored
1.3 穷尽匹配
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
的情况,因此会报错.
1.4 _
通配符
当我们不想在匹配时列出所有值的时候,可以使用 Rust
提供的一个特殊模式 ,例如,u8
可以拥有 0 到
255 的有效的值,但是我们只关心 1、3、5 和 7
这几个值,不想列出其它的 0、2、4、6、8、9 一直到 255
的值。那么, 我们不必一个一个列出所有值, 因为可以使用特殊的模式
_
替代:
1 2 3 4 5 6 7 8 let some_u8_value = 0u8 ;match some_u8_value { 1 => println! ("one" ), 3 => println! ("three" ), 5 => println! ("five" ), 7 => println! ("seven" ), _ => (), }
通过将 _
其放置于其他分支后,_
将会匹配所有遗漏的值。()
表示返回单元类型 与所有分支返回值的类型相同,所以当匹配到
_
后,什么也不会发生。
除了_
通配符,用一个变量来承载其他情况也是可以的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #[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
就显得过于啰嗦。
2. if let
匹配
有时会遇到只有一个模式的值需要被处理,其它值直接忽略的场景,如果用
match
来处理就要写成下面这样:
1 2 3 4 5 let v = Some (3u8 );match v { Some (3 ) => println! ("three" ), _ => (), }
我们只想要对 Some(3)
模式进行匹配, 不想处理任何其他
Some<u8>
值或 None
值。但是为了满足
match
表达式(穷尽性)的要求,写代码时必须在处理完这唯一的成员后加上
_ => ()
,这样会增加不少无用的代码。
俗话说“杀鸡焉用牛刀”,我们完全可以用 if let
的方式来实现:
1 2 3 if let Some (3 ) = v { println! ("three" ); }
当你只要匹配一个条件,且忽略其他条件时就用
if let
,否则都用 match
。
3. matches!宏
Rust
标准库中提供了一个非常实用的宏:matches!
,它可以将一个表达式跟模式进行匹配,然后返回匹配的结果
true
or false
。
例如,有一个动态数组,里面存有以下枚举:
1 2 3 4 5 6 7 8 enum MyEnum { Foo, Bar }fn main () { let v = vec! [MyEnum::Foo,MyEnum::Bar,MyEnum::Foo]; }
现在如果想对 v
进行过滤,只保留类型是
MyEnum::Foo
的元素,你可能想这么写:
1 v.iter ().filter (|x| x == MyEnum::Foo);
但是,实际上这行代码会报错,因为你无法将 x
直接跟一个枚举成员进行比较。好在,你可以使用 match
来完成,但是会导致代码更为啰嗦,是否有更简洁的方式?答案是使用
matches!
:
1 v.iter ().filter (|x| matches!(x, MyEnum::Foo));
很简单也很简洁,再来看看更多的例子:
1 2 3 4 5 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
,这里都是一个新的代码块,而且这里的绑定相当于新变量,如果你使用同名变量,会发生变量遮蔽:
1 2 3 4 5 6 7 8 9 fn main () { let age = Some (30 ); println! ("在匹配前,age是{:?}" ,age); if let Some (age) = age { println! ("匹配出来的age是{}" ,age); } println! ("在匹配后,age是{:?}" ,age); }
cargo run
运行后输出如下:
1 2 3 在匹配前,age是Some(30) 匹配出来的age是30 在匹配后,age是Some(30)
可以看出在 if let
中,=
右边
Some(i32)
类型的 age
被左边 i32
类型的新 age
遮蔽了,该遮蔽一直持续到 if let
语句块的结束。因此第三个 println!
输出的 age
依然是 Some(i32)
类型。
对于 match
类型也是如此:
1 2 3 4 5 6 7 8 9 fn main () { let age = Some (30 ); println! ("在匹配前,age是{:?}" ,age); match age { Some (age) => println! ("匹配出来的age是{}" ,age), _ => () } println! ("在匹配后,age是{:?}" ,age); }
需要注意的是,match
中的变量遮蔽其实不是那么的容易看出 ,因此要小心!其实这里最好不要使用同名,避免难以理解,如下。
1 2 3 4 5 6 7 8 9 fn main () { let age = Some (30 ); println! ("在匹配前,age是{:?}" , age); match age { Some (x) => println! ("匹配出来的age是{}" , x), _ => () } println! ("在匹配后,age是{:?}" , age); }