Rust:模式匹配詳解
Rust 的模式匹配詳解
Rust 中經常使用到的一個功能是模式匹配,例如 let 變量賦值本質上就是模式匹配。
官方手冊參考:https://doc.rust-lang.org/reference/patterns.html。
模式匹配的使用場景
可在如下幾種情況下使用模式匹配:
-
let 變量賦值
-
函數參數傳值時的模式匹配
-
match 分支
-
if let
-
while let
-
for 循環的模式匹配
let 變量賦值時的模式匹配
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 分支匹配的用法非常靈活。它的語法爲:
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
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
的模式匹配成功,就會一直執行 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 循環退出。
for 循環
這個無需解釋。一個示例即可:
let v = vec!['a','b','c'];
for (idx, value) in v.iter().enumerate(){
println!("{}: {}", idx, value);
}
模式的兩種形式:refutable 和 irrefutable
從前面介紹的幾種模式匹配可瞭解到,模式匹配的方式不唯一,有的時候是一定匹配成功的變量賦值型 (let/for / 函數傳參) 模式匹配,有的時候是可能匹配失敗的模式匹配。
Rust 中爲這兩種定義了專門的稱呼:
-
不可反駁的模式 (irrefutable):一定會匹配成功,否則編譯錯誤
-
可反駁的的模式 (refutable):可以匹配成功,也可以匹配失敗,匹配失敗的結果是不執行對應分支的代碼
**「let 變量賦值、for 循環、函數傳參」這三種模式匹配只接受不可反駁模式。「if let 和 while let」**只接受可反駁模式。
**「match」**支持兩種模式:
-
當明確給出分支的 Pattern 時,必須是可反駁模式,這些模式允許匹配失敗
-
使用
_
作爲最後一個分支時,是不可反駁模式,它一定會匹配成功 -
如果只有一個 Pattern 分支,則可以是不可反駁模式,也可以是可反駁模式
當模式匹配處使用了不接受的模式時,將會編譯錯誤或給出警告。
// 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 支持數值和字符的範圍,有如下幾種範圍表達式:
但範圍作爲模式時,只允許全閉合的..=
範圍,其他類型的範圍都會報錯。
例如:
// 數值範圍
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
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
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),
}
ref 和 mut 修飾模式中的變量
當進行解構賦值時,很可能會將變量擁有的所有權轉移出去,從而使得原始變量變得不完整或直接失效。
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);
}
匹配守衛 (match guard)
匹配守衛允許匹配分支添加**「額外的後置」**條件:當匹配了某分支的模式後,再檢查該分支的守衛後置條件,如果守衛條件也通過,則成功匹配該分支。
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"),
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/D7xLKZXt8OfbvtCtsUW3mQ