Rust:模式匹配詳解

Rust 的模式匹配詳解

Rust 中經常使用到的一個功能是模式匹配,例如 let 變量賦值本質上就是模式匹配。

官方手冊參考:https://doc.rust-lang.org/reference/patterns.html。

模式匹配的使用場景

可在如下幾種情況下使用模式匹配:

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 ifelse if letelse一起使用。

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 中爲這兩種定義了專門的稱呼:

**「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 支持數值和字符的範圍,有如下幾種範圍表達式:

WRSVTx

但範圍作爲模式時,只允許全閉合的..=範圍,其他類型的範圍都會報錯。

例如:

// 數值範圍
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"),
(| 5 | 6) if y => println!("yes"),

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/D7xLKZXt8OfbvtCtsUW3mQ