所有可能會用到模式的位置

模式出現在 Rust 的很多地方。你已經在不經意間使用了很多模式!本節將介紹所有模式有效的位置。

match 分支

如第六章所討論的,一個模式常用的位置是 match 表達式的分支。在形式上 match 表達式由 match 關鍵字、用於匹配的值和一個或多個分支構成,這些分支包含一個模式和在值匹配分支的模式時運行的表達式,如下所示:

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

例如這是一個來自示例 6-5 中匹配變量 xOption<i32> 值的 match 表達式:

match x {
    None => None,
    Some(i) => Some(i + 1),
}

這個 match 表達式中的模式為每個箭頭左邊的 NoneSome(i)

match 表達式的一個要求是它們必須是窮盡exhaustive)的,意為 match 表達式所有可能的值都必須被考慮到。一個確保覆蓋每個可能值的方法是在最後一個分支使用捕獲所有的模式:比如,一個匹配任何值的名稱永遠也不會失敗,因此可以覆蓋所有匹配剩下的情況。

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

if let 條件表達式

第六章討論過了 if let 表達式,以及它是如何主要用於編寫等同於只關心一個情況的 match 語句簡寫的。if let 可以對應一個可選的帶有代碼的 elseif let 中的模式不匹配時運行。

示例 19-1 展示了也可以組合並匹配 if letelse ifelse if let 表達式。這相比 match 表達式一次只能將一個值與模式比較提供了更多靈活性。並且 Rust 並不要求一系列 if letelse ifelse if let 分支的條件相互關聯。

示例 19-1 中的代碼展示了一系列針對不同條件的檢查來決定背景顏色應該是什麼。為了達到這個例子的目的,我們創建了硬編碼值的變量,真實程序中這些值可能來源於用户輸入。

文件名:src/main.rs

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-01/src/main.rs}}

示例 19-1: 結合 if letelse ifelse if let 以及 else

如果用户指定了中意的顏色,將使用其作為背景顏色。如果沒有指定中意的顏色且今天是星期二,背景顏色將是綠色。如果用户指定了他們的年齡字符串並能夠成功將其解析為數字的話,我們將根據這個數字使用紫色或者橙色。最後,如果沒有一個條件符合,背景顏色將是藍色。

這個條件結構允許我們支持複雜的需求。使用這裏硬編碼的值,例子會打印出 Using purple as the background color

注意 if let 也可以像 match 分支那樣引入並遮蔽現有變量:if let Ok(age) = age 引入了一個新的 age 變量,包含 Ok 變體中的值,從而遮蔽了之前的 age 變量。這意味着 if age > 30 條件需要位於這個代碼塊內部:不能將兩個條件組合為 if let Ok(age) = age && age > 30,因為我們想與 30 比較的新 age 只有在大括號開啓的新作用域內才有效。

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

while let 條件循環

一個與 if let 結構類似的是 while let 條件循環,它允許只要模式匹配就一直進行 while 循環。在示例 19-2 展示了一個 while let 循環等待跨線程發送的消息,不過在這個示例中它檢查一個 Result 而非 Option

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-02/src/main.rs:here}}

示例 19-2: 使用 while let 循環只要 rx.recv() 返回 Ok 就打印出其值

這個例子會打印出 123recv 方法從信道的接收端取出第一條消息並返回一個 Ok(value)。當在第十六章遇到 recv 時,我們直接 unwrap 了錯誤,或者使用 for 循環將其視為迭代器處理。不過如示例 19-2 所示,我們也可以使用 while let,因為 recv 方法只要發送端持續產生消息它就一直返回 Ok,並在發送端斷開連接後產生一個 Err

for 循環

for 循環中,模式是 for 關鍵字直接跟隨的值。例如,在 for x in y 中,x 就是這個模式。示例 19-3 中展示瞭如何使用 for 循環來解構,或拆開一個元組作為 for 循環的一部分:

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-03/src/main.rs:here}}

示例 19-3: 在 for 循環中使用模式來解構元組

示例 19-3 的代碼會打印出:

{{#include ../listings/ch19-patterns-and-matching/listing-19-03/output.txt}}

這裏使用 enumerate 方法適配一個迭代器來產生一個值和其在迭代器中的索引,它們位於一個元組中。第一個產生的值是元組 (0, 'a')。當這個值匹配模式 (index, value)index 將會是 0value 將會是 'a',並打印出第一行輸出。

let 語句

在本章之前,我們只明確討論過通過 matchif let 使用模式,不過事實上也在別的地方使用過模式,包括 let 語句。例如,考慮一下這個直白的 let 變量賦值:

let x = 5;

不過你可能沒有發覺,每一次像這樣使用 let 語句就是在使用模式!let 語句更為正式的樣子如下:

let PATTERN = EXPRESSION;

let x = 5; 這樣的語句中變量名位於 PATTERN 位置,變量名不過是形式特別樸素的模式。我們將表達式與模式比較,併為任何找到的名稱賦值。所以例如 let x = 5; 的情況,x 是一個代表 “將匹配到的值綁定到變量 x” 的模式。同時因為名稱 x 是整個模式,這個模式實際上等於 “將任何值綁定到變量 x,不管值是什麼”。

為了更清楚的理解 let 的模式匹配方面的內容,考慮示例 19-4 中使用 let 和模式解構一個元組:

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-04/src/main.rs:here}}

示例 19-4: 使用模式解構元組並一次創建三個變量

這裏將一個元組與模式匹配。Rust 會比較值 (1, 2, 3) 與模式 (x, y, z),並發現二者具有相同的元素數量,因此匹配成功,於是將 1 綁定到 x,將 2 綁定到 y,將 3 綁定到 z。你可以將這個元組模式看作是將三個獨立的變量模式結合在一起。

如果模式中元素的數量不匹配元組中元素的數量,則整個類型不匹配,並會得到一個編譯時錯誤。例如,示例 19-5 展示了嘗試用兩個變量解構三個元素的元組,這是不行的:

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-05/src/main.rs:here}}

示例 19-5: 一個錯誤的模式結構,其中變量的數量不符合元組中元素的數量

嘗試編譯這段代碼會給出如下類型錯誤:

{{#include ../listings/ch19-patterns-and-matching/listing-19-05/output.txt}}

為了修復這個錯誤,可以使用 _.. 來忽略元組中一個或多個值,如 [“忽略模式中的值”][ignoring-values-in-a-pattern] 部分所示。如果問題是模式中有太多的變量,則解決方法是通過去掉變量使得變量數與元組中元素數相等。

函數參數

函數參數也可以是模式。示例 19-6 中的代碼聲明瞭一個叫做 foo 的函數,它獲取一個 i32 類型的參數 x,現在這看起來應該很熟悉:

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-06/src/main.rs:here}}

示例 19-6: 在參數中使用模式的函數簽名

x 部分就是一個模式!類似於之前對 let 所做的,可以在函數參數中匹配元組。示例 19-7 將傳遞給函數的元組拆分為各個值:

文件名:src/main.rs

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-07/src/main.rs}}

示例 19-7: 一個在參數中解構元組的函數

這會打印出 Current location: (3, 5)。值 &(3, 5) 會匹配模式 &(x, y),如此 x 得到了值 3,而 y 得到了值 5

因為如第十三章所講閉包類似於函數,也可以在閉包參數列表中使用模式。

現在我們見過了很多使用模式的方式了,不過模式在每個使用它的地方並不以相同的方式工作;在一些地方,模式必須是 irrefutable 的,意味着它們必須匹配所提供的任何值。在另一些情況,它們則可以是 refutable 的。接下來讓我們討論這兩個概念。


Refutability(可反駁性): 模式是否會匹配失效

模式有兩種形式: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 要求不可反駁模式的地方使用可反駁模式以及相反情況的例子。在示例 19-8 中,有一個 let 語句,不過模式被指定為可反駁模式 Some(x)。如你所見,這不能編譯:

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-08/src/main.rs:here}}

示例 19-8: 嘗試在 let 中使用可反駁模式

如果 some_option_value 的值是 None,其不會成功匹配模式 Some(x),表明這個模式是可反駁的。然而,因為 let 對於 None 匹配不能產生任何合法的代碼,所以 let 語句只能接受不可反駁模式。Rust 會在編譯時抱怨我們嘗試在要求不可反駁模式的地方使用可反駁模式:

{{#include ../listings/ch19-patterns-and-matching/listing-19-08/output.txt}}

因為我們沒有覆蓋(也不可能覆蓋!)到模式 Some(x) 的每一個可能的值,所以 Rust 會合理地抗議。

為了修復在需要不可反駁模式的地方使用可反駁模式的情況,可以修改使用模式的代碼:不同於使用 let,可以使用 if let。如此,如果模式不匹配,大括號中的代碼將被忽略,其餘代碼保持有效。示例 19-9 展示瞭如何修復示例 19-8 中的代碼。

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-09/src/main.rs:here}}

示例 19-9: 使用 if let 和一個帶有可反駁模式的代碼塊來代替 let

我們給代碼留了一條後路!現在這段代碼已經完全有效了。然而,如果我們給 if let 提供一個不可反駁模式(即總會匹配的模式),例如示例 19-10 中的 x,編譯器就會給出警告:

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-10/src/main.rs:here}}

示例 19-10: 嘗試把不可反駁模式用到 if let

Rust 會抱怨將不可反駁模式用於 if let 是沒有意義的:

{{#include ../listings/ch19-patterns-and-matching/listing-19-10/output.txt}}

基於此,match 匹配分支必須使用可反駁模式,除了最後一個分支需要使用能匹配任何剩餘值的不可反駁模式。Rust 允許我們在只有一個匹配分支的match 中使用不可反駁模式,不過這麼做不是特別有用,並可以被更簡單的 let 語句替代。

目前我們已經討論了所有可以使用模式的地方,以及可反駁模式與不可反駁模式的區別,下面讓我們一起去把可以用來創建模式的語法過目一遍吧。