2020年6月22日

rust18_Pattern Matching

pattern 是 rust 的特殊語法,用來匹配類別的 struct,結合 match expression 及其他 struct 可提供更多程式 control flow 的支配權。Pattern 由以下內容組成

  • Literals
  • Destructured arrays, enums, structs, or tuples
  • Variables
  • Wildcards
  • Placeholders

這些元件就是要處理的 data,接著可用匹配值決定程式是否有正確的資料,運作特定部分的 code

透過一些值與 pattern 比較來使用它們,如果匹配,就做對應處理。

以下討論 pattern 適用處,refutableirrefutable 模式的區別,及不同類別的 pattern 語法

所有可能用到 Pattern 的地方

match Arms

match 表達式由 match 關鍵字、用於匹配的值和一個或多個分支構成,這些分支包含一個模式和在值匹配分支的模式時運行的表達式:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

match 表達式必須是 窮盡exhaustive)的,所有可能的值都必須被考慮到。

有一個特定的模式 _ 可以匹配所有情況,不過它從不綁定任何變數。這在例如希望忽略任何未指定值的情況很有用。本章之後的 “在模式中忽略值” 部分會詳細介紹 _ 模式的更多細節。

Conditional if let Expression

chap6 討論過 if let 及如何撰寫只關心一個狀況的match 語法的簡寫。if let 可對應一個 optional 帶有 code 的 else,在 if let pattern 不匹配時運作。

可以組合併匹配 if letelse ifelse if let 表達式。相比 match表達式一次只能將一個值與模式比較提供了更多靈活性;一系列 if letelse ifelse if let 分支並不要求其條件相互關聯。

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 {
        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");
    }
}

注意 if let 也可以像 match 分支那樣引入 shadowed 變數:if let Ok(age) = age 引入了一個新的 shadowed 變數 age,它包含 Ok 成員中的值。這意味著 if age > 30 條件需要位於這個代碼塊內部;不能將兩個條件組合為 if let Ok(age) = age && age > 30,因為我們希望與 30 進行比較的被覆蓋的 age 直到大括號開始的新作用域才是有效的。

if let 表達式的缺點在於無法用 compiler 檢查窮盡性,而 match 表達式有做檢查。如果去掉最後的 else 塊而遺漏處理一些情況,編譯器也不會警告這類可能的邏輯錯誤。

while let Conditional Loop

只要 stack.pop() 回傳 Some 就列印

let mut stack = Vec::new();

stack.push(1);
stack.push(2);
stack.push(3);

while let Some(top) = stack.pop() {
    println!("{}", top);
}

for Loop

for 可以獲取一個模式,例如在 for 循環中使用模式來解構元組

enumerate 方法產生一個迭代器,得到一個值和其在迭代器中的索引,他們位於一個元組中。第一個 enumerate 呼叫會產生元組 (0, 'a')。當這個值匹配模式 (index, value)index將會是 0 而 value 將會是 'a',並印出第一行輸出。

let v = vec!['a', 'b', 'c'];

for (index, value) in v.iter().enumerate() {
    println!("{} is at index {}", value, index);
}

let statement

let x = 5; 其實就是 pattern,也就是 let PATTERN = EXPRESSION;

這個模式實際上等於 “將任何值綁定到變數 x,不管值是什麼”

// 將一個 tuple 與模式匹配
let (x, y, z) = (1, 2, 3);

function parameters

函數參數也可以是模式,x 部分就是一個模式

fn foo(x: i32) {
    // 代碼
}

ex:

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

closure 類似 function,故也可以在 closure parameter 使用 pattern

在一些地方,模式必須是 irrefutable 的,必須匹配所提供的任何值。在另一些情況,他們則可以是 refutable 的。

Refutability: 是否 pattern 會匹配失敗

模式有兩種形式:refutable(可反駁的)和 irrefutable(不可反駁的)。能匹配任何傳遞的可能值的模式被稱為是 不可反駁的irrefutable)。例如 let x = 5; 語句中的 x,因為 x 可以匹配任何值所以不可能會失敗。

對某些可能的值進行匹配會失敗的模式被稱為是 可反駁的refutable)。一個這樣的例子便是 if let Some(x) = a_value 表達式中的 Some(x);如果變量 a_value 中的值是 None 而不是 Some,那麼 Some(x) 模式不能匹配。

let 語句、 函數參數和 for 循環只能接受不可反駁的模式,因為通過不匹配的值程序無法進行有意義的工作。if letwhile let 表達式被限制為只能接受可反駁的模式,因為根據定義就是要處理可能的失敗:條件表達式的功能就是根據成功或失敗執行不同的操作。

通常無需擔心可反駁和不可反駁模式的區別,不過確實需要熟悉可反駁性的概念,這樣當在錯誤信息中看到時就知道如何應對。遇到這些情況,根據代碼行為的意圖,需要修改模式或者使用模式的結構。

一個嘗試在 Rust 要求不可反駁模式的地方使用可反駁模式以及相反情況的例子

let Some(x) = some_option_value;

會發生編譯錯誤

error[E0005]: refutable pattern in local binding: `None` not covered
 -->
  |
3 | let Some(x) = some_option_value;
  |     ^^^^^^^ pattern `None` not covered

為了修復在需要不可反駁模式的地方使用可反駁模式的情況,可以修改使用模式的代碼:不同於使用 let,可以使用 if let

if let Some(x) = some_option_value {
    println!("{}", x);
}

如果為 if let 提供了一個總是會匹配的模式,嘗試把不可反駁模式用到 if let 上,比如以下的 x,則會出錯:

if let x = 5 {
    println!("{}", x);
};

會發生編譯錯誤

error[E0162]: irrefutable if-let pattern
 --> <anon>:2:8
  |
2 | if let x = 5 {
  |        ^ irrefutable pattern

匹配分支必須使用可反駁模式,除了最後一個分支需要使用能匹配任何剩餘值的不可反駁模式。

Pattern Syntax

以下列出所有 pattern 的有效語法

matching literals

let x = 1;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    _ => println!("anything"),
}

matching named variables

Named variables 是 irrefutable(不可反駁的)可 match any value

match 會開始一個新作用域,match 表達式中作為模式的一部分聲明的變數會覆蓋 match 結構之外的同名變數,與所有變數一樣。

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        // y 是 shaowed variable,跟上面的 y 無關,可匹配任何 Some 裡面的 value
        // 當 scope 結束,y 的作用域就結束
        Some(y) => println!("Matched, y = {:?}", y),
        _ => println!("Default case, x = {:?}", x),
    }

    // 這裡的 y 還是一樣是 10
    println!("at the end: x = {:?}, y = {:?}", x, y);
}

multiple patterns

match 表達式中,可以使用 | 語法匹配多個模式,它代表 or 的意思。

let x = 1;

match x {
    // 符合 1 或 2
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

matching rangers of values with

... 語法允許你匹配一個閉區間範圍內的值。

let x = 5;

match x {
    // 如果 x 是 1、2、3、4 或 5,就會匹配
    1 ... 5 => println!("one through five"),
    _ => println!("something else"),
}

rangers 範圍只允許用於數字或 char

let x = 'c';

match x {
    'a' ... 'j' => println!("early ASCII letter"),
    'k' ... 'z' => println!("late ASCII letter"),
    _ => println!("something else"),
}

destructing to break apart values

可以使用模式來解構 structs、enums、tuples 和引用,以便使用這些值的不同部分。

  • destructing structs

    可以通過帶有模式的 let 語句將其分解:

    struct Point {
        x: i32,
        y: i32,
    }
    
    fn main() {
        let p = Point { x: 0, y: 7 };
    
        let Point { x: a, y: b } = p;
        assert_eq!(0, a);
        assert_eq!(7, b);
    }

    這個例子展示了模式中的變數名不必與結構體中的欄位一致。不過通常希望變數名與欄位名一致以便於理解變數來自於哪些欄位。

    因為變數名匹配欄位名是常見的,同時因為 let Point { x: x, y: y } = p; 包含了很多重複,所以對於匹配欄位的模式存在簡寫:只需列出結構體欄位的名稱,則模式產生的變數會有相同的名稱。

    struct Point {
        x: i32,
        y: i32,
    }
    
    fn main() {
        let p = Point { x: 0, y: 7 };
    
        let Point { x, y } = p;
        assert_eq!(0, x);
        assert_eq!(7, y);
    }

    也可以在部分struct 模式中使用 literal 進行解析,而不是為所有的欄位產生變數。

    fn main() {
        let p = Point { x: 0, y: 7 };
    
        match p {
            // x 軸
            Point { x, y: 0 } => println!("On the x axis at {}", x),
            // y 軸
            Point { x: 0, y } => println!("On the y axis at {}", y),
            Point { x, y } => println!("On neither axis: ({}, {})", x, y),
        }
    }
  • destructing enums

    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }
    
    fn main() {
        let msg = Message::ChangeColor(0, 160, 255);
    
        match msg {
            // Message::Quit 這樣沒有任何資料的enum,不能進一步解構其值。只能匹配其 literal Message::Quit,因此模式中沒有任何變數
            Message::Quit => {
                println!("The Quit variant has no data to destructure.")
            },
            // 使用大括號並列出欄位變數,以便將其分解以供此分支的代碼使用
            Message::Move { x, y } => {
                println!(
                    "Move in the x direction {} and in the y direction {}",
                    x,
                    y
                );
            }
            Message::Write(text) => println!("Text message: {}", text),
            Message::ChangeColor(r, g, b) => {
                println!(
                    "Change the color to red {}, green {}, and blue {}",
                    r,
                    g,
                    b
                )
            }
        }
    }
  • destructing reference

    當模式所匹配的值中包含引用時,需要解構引用之中的值,這可以通過在模式中指定 & 做到。這讓我們得到一個包含引用所指向資料的變數,而不是包含引用的變數。這個技術在通過迭代器遍歷引用時,要使用 closure 中的值而不是其引用時非常有用。

    遍歷一個 vector 中的 Point 實例的引用,並同時解構引用和其中的 struct 以方便對 xy 值進行計算

    let points = vec![
        Point { x: 0, y: 0 },
        Point { x: 1, y: 5 },
        Point { x: 10, y: -3 },
    ];
    
    let sum_of_squares: i32 = points
        .iter()
        .map(|&Point { x, y }| x * x + y * y)
        .sum();
  • destructing nested structs and enums

    以匹配嵌套的 enum

    enum Color {
       Rgb(i32, i32, i32),
       Hsv(i32, i32, i32)
    }
    
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(Color),
    }
    
    fn main() {
        let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
    
        match msg {
            Message::ChangeColor(Color::Rgb(r, g, b)) => {
                println!(
                    "Change the color to red {}, green {}, and blue {}",
                    r,
                    g,
                    b
                )     
            },
            Message::ChangeColor(Color::Hsv(h, s, v)) => {
                println!(
                    "Change the color to hue {}, saturation {}, and value {}",
                    h,
                    s,
                    v
                )
            }
            _ => ()
        }
    }
  • destructing nested structs and tuples

    struct 和 tuple 嵌套在 tuple 中

    let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });

Ignoring values in a pattern

有時忽略模式中的一些值是有用的,例如 match 中最後捕獲全部情況的分支實際上沒有做任何事,但是它確實對所有剩餘情況負責。有一些簡單的方法可以忽略模式中全部或部分值:使用 _ 模式,使用一個以下劃線開始的名稱,或者使用 ..忽略所剩部分的值。

  • ignoring an entire value with _

    _ 也可以用在函數的參數上

    fn foo(_: i32, y: i32) {
        println!("This code only uses the y parameter: {}", y);
    }
    
    fn main() {
        foo(3, 4);
    }
  • ignoring parts of a value with a nested _

    只需要測試部分值但在期望運行的代碼部分中沒有使用它們時,也可以在另一個模式內部使用 _來只忽略部分值。

    let mut setting_value = Some(5);
    let new_setting_value = Some(10);
    
    match (setting_value, new_setting_value) {
        // 不需要 Some 中的值時
        (Some(_), Some(_)) => {
            println!("Can't overwrite an existing customized value");
        }
        _ => {
            setting_value = new_setting_value;
        }
    }

    也可以在一個模式中的多處使用 underline 來忽略特定值

    let numbers = (2, 4, 8, 16, 32);
    
    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {}, {}, {}", first, third, fifth)
        },
    }
  • ignoring an unused variable by starting its name with _

    產生了一個變量卻不在任何地方使用它,Rust 會給你一個警告,因為這可能會是個 bug。但是有時是有用的,例如你正在設計原型或剛剛開始一個 project。這時你希望告訴 Rust 不要警告未使用的變量,為此可以用 underline 作為變數名的開頭。

    fn main() {
        let _x = 5;
        let y = 10;
    }

    只使用 _ 和使用以下劃線開頭的名稱有些微妙的不同:比如 _x 仍會將值綁定到變數,而 _則完全不會綁定。以下會得到一個編譯錯誤,因為 s 的值仍然會移動進 _s,並阻止我們再次使用 s

    let s = Some(String::from("Hello!"));
    
    if let Some(_s) = s {
        println!("found a string");
    }
    
    println!("{:?}", s);

    要改寫為

    let s = Some(String::from("Hello!"));
    
    if let Some(_) = s {
        println!("found a string");
    }
    
    println!("{:?}", s);
  • ignoring remaining parts of a value with ..

    對於有多個部分的值,可以使用 .. 語法來只使用部分並忽略其它值,同時避免不得不對每一個忽略值列出下劃線。.. 模式會忽略模式中剩餘的任何沒有顯式匹配的值部分。以下有一個 Point 存放了三維空間中的坐標。在 match 表達式中,我們希望只操作 x 座標並忽略 yz 的值:

    struct Point {
        x: i32,
        y: i32,
        z: i32,
    }
    
    let origin = Point { x: 0, y: 0, z: 0 };
    
    match origin {
        // 忽略 Point 中除 x 以外的 fields
        Point { x, .. } => println!("x is {}", x),
    }
    
    fn main() {
        let numbers = (2, 4, 8, 16, 32);
    
        match numbers {
            // 只匹配 tuple 中的第一個和最後一個值並忽略掉所有其它值
            (first, .., last) => {
                println!("Some numbers: {}, {}", first, last);
            },
        }
    }

    如果期望匹配和忽略的值是不明確的,Rust 會給編譯錯誤。

    fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {}", second)
        },
    }
}

Extra Conditionals with Match Guards

匹配守衛match guard)是一個指定與 match 分支模式之後的額外 if 條件,它也必須被滿足才能選擇此分支。匹配守衛用於表達比單獨的模式所能允許的更為複雜的情況。

let num = Some(4);

match num {
    // 判斷 x 是否 < 5
    Some(x) if x < 5 => println!("less than five: {}", x),
    Some(x) => println!("{}", x),
    None => (),
}

可以使用match guard來解決模式中 shadowed 變數的問題,那裡 match 表達式的模式中產生了一個變數而不是使用 match 之外的同名變數。新變數就代表不能夠測試外部變數的值。可使用匹配守衛修復這個問題:

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        // 用 matching guard 測試跟外部變數是否相等
        // n == y 沒有產生新變數,這裡的 y 就是外部的 y
        Some(n) if n == y => println!("Matched, n = {:?}", n),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {:?}", x, y);
}
let x = 4;
let y = false;

match x {
    // 也可以使用 或 運算符 | 來指定多個模式,同時 match guard 的條件會作用域所有的模式
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
}

@ Bindings

at@)讓我們在產生一個存放值的變數的同時測試其值是否匹配模式。

enum Message {
    Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    // 測試 Message::Hello 的 id 字段是否位於 3...7 範圍內,同時也希望能綁定其值到 id_variable 中以便此分支相關聯的 code 可以使用它
    Message::Hello { id: id_variable @ 3...7 } => {
        println!("Found an id in range: {}", id_variable)
    },
    // 指定了一個範圍 10, 11, 12,但裡面不能使用 id,因為沒有將 id 儲存到另一個變數中
    Message::Hello { id: 10...12 } => {
        println!("Found an id in another range")
    },
    // 沒有範圍的變數
    Message::Hello { id } => {
        println!("Found some other id: {}", id)
    },
}

References

The Rust Programming Language

中文版

中文版 2

沒有留言:

張貼留言