模式解構賦值

模式匹配時可用於解構賦值,可解構的類型包括struct、enum、tuple、slice等等。

解構賦值時,可使用_作為某個變量的佔位符,使用..作為剩餘所有變量的佔位符(使用..時不能產生歧義,例如(..,x,..)是有歧義的)。當解構的類型包含了命名字段時,可使用fieldname簡化fieldname: fieldname的書寫。

解構struct

解構Struct時,會將待解構的struct各個字段和Pattern中的各個字段進行匹配,併為找到的字段變量進行賦值。

當Pattern中的字段名和字段變量同名時,可簡寫。例如P{name: name, age: age}P{name, age}是等價的Pattern。

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;
  println!("x: {}, y: {}", x, y);
  // 解構時可修改字段變量名: let Point2{x: a, y: b} = p;
  // 此時,變量a和b將被賦值
   
  let ori = Point{x: 0, y: 0, z: 0};
  match ori{
    // 使用..忽略解構後剩餘的字段
    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),
  }
}

解構元組


#![allow(unused)]
fn main() {
let ((feet, inches), Point {x, y}) = ((3, 1), Point { x: 3, y: -1 });
}

@綁定變量名

當解構後進行模式匹配時,如果某個值沒有對應的變量名,則可以使用@手動綁定一個變量名。

例如:


#![allow(unused)]
fn main() {
struct S(i32, i32);

match S(1, 2) {
  // 如果匹配1成功,將其賦值給變量z
  // 如果匹配2成功,也將其賦值給變量z
    S(z @ 1, _) | S(_, z @ 2) => assert_eq!(z, 1),
    _ => panic!(),
}
}

再例如,匹配並解構一個數組:


#![allow(unused)]
fn main() {
let arr = ["x", "y", "z"];
match arr {
  [.., "!"] => println!("!!!"),
  // 匹配成功,start = ["x", "y"]
  [start @ .., "z"] => println!("starts: {:?}", start),
  ["a", end @ ..] => println!("ends: {:?}", 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字段所有權已轉移
}

如果想要在解構賦值時不丟失所有權,有以下幾種方式:


#![allow(unused)]
fn main() {
// 方式一:解構表達式的引用
let Person{name, age} = &p;

// 方式二:解構表達式的克隆,適用於可調用clone()方法的類型
// 但Person struct沒有clone()方法

// 方式三:在模式的某些字段或元素上使用ref關鍵字修飾變量
let Person{ref name, age} = p;
let Person{name: ref n, age} = p;
}

在模式中使用ref修飾變量名相當於對被解構的字段或元素上使用&進行引用。


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

注意,使用ref修飾變量只是借用了被解構表達式的一部分值,而不是借用整個值。如果要匹配的是一個引用,則使用&


#![allow(unused)]
fn main() {
let a = &(1,2,3);       // a是一個引用
let (t1,t2,t3) = a;     // t1,t2,t3都是引用類型&i32
let &(x,y,z) = a;       // x,y,z都是i32類型
let &(ref xx,yy,zz) = a;  // xx是&i32類型,yy,zz是i32類型
}

最後,也可以將match value{}的value進行修飾,例如match &mut value {},這樣就不需要在模式中去加ref和mut了。這對於有多個分支需要解構賦值,且每個模式中都需要ref/mut修飾變量的match非常有用。

fn main() {
  let mut s = "hello".to_string();
  match &mut s {   // 對可變引用進行匹配
    // 匹配成功時,變量也是對原數據的可變引用
    x => x.push_str("world"),
  }
  println!("{}", s);
}

匹配守衛(match guard)

匹配守衛允許匹配分支添加額外的後置條件:當匹配了某分支的模式後,再檢查該分支的守衛後置條件,如果守衛條件也通過,則成功匹配該分支。


#![allow(unused)]
fn main() {
let x = 33;
match x {
  // 先範圍匹配,範圍匹配成功後,再檢查是否是偶數
  // 如果範圍匹配沒有成功,則不會檢查後置條件
  0..=50 if x % 2 == 0 => {
    println!("x in [0, 50], and it is an even");
  },
  0..=50 => println!("x in [0, 50], but it is not an even"),
  _ => (),
}
}

注意,後置條件的優先級很低。例如:


#![allow(unused)]
fn main() {
// 下面兩個分支的寫法等價
4 | 5 | 6 if bool_expr => println!("yes"),
(4 | 5 | 6) if bool_expr => println!("yes"),
}

注意(1):對引用進行解構賦值時

在解構賦值時,如果解構的是一個引用,則被匹配的變量也將被賦值為對應元素的引用


#![allow(unused)]
fn main() {
let t = &(1,2,3);    // t是一個引用
let (t0,t1,t2) = t;  // t0,t1,t2的類型都是&i32
let t0 = t.0;   // t0的類型是i32而不是&i32,因為t.0等價於(*t).0
let t0 = &t.0;  // t0的類型是&i32而不是i32,&t.0等價於&(t.0)而非(&t).0
}

因此,當使用模式匹配語法for i in t進行迭代時:

  • 如果t不是一個引用,則t的每一個元素都會move給i
  • 如果t是一個引用,則i將是每一個元素的引用
  • 同理,for i in &mut tfor i in mut t也一樣

注意(2):對解引用進行匹配時

match VALUE的VALUE是一個解引用*xyz時(因此,xyz是一個引用),可能會發生所有權的轉移,此時可使用xyz&*xyz來代替*xyz。具體原因請參考:解引用(deref)的所有權轉移問題

下面是一個示例:

fn main() {
  // p是一個Person實例的引用
  let p = &Person {
    name: "junmajinlong".to_string(),
    age: 23,
  };
  
  // 使用&*p或p進行匹配,而不是*p
  // 使用*p將報錯,因為會轉移所有權
  match &*p {
    Person {name, age} =>{
      println!("{}, {}",name, age);
    },
    _ => (),
  }
}

struct Person {
  name: String,
  age: u8,
}