模式語法
在此段落中,我們會收集所有模式中的有效語法,並討論你會怎麼使用它們。
配對字面值
如同你在第六章所見的,你可以直接使用字面值來配對模式,以下程式碼展示了一些範例:
fn main() { let x = 1; match x { 1 => println!("一"), 2 => println!("二"), 3 => println!("三"), _ => println!("任意數字"), } }
此程式碼會顯示「一」因為 x 的數值為 1。此語法適用於當你想要程式碼取得一個特定數值時,就馬上採取行動的情況。
配對變數名稱
變數名稱是能配對任何數值的不可反駁模式,而且我們在本書中已經使用非常多次。不過當你在 match
表達式中使用變數名稱時會複雜一點。因為 match
會初始一個新的作用域,作為 match
表達式部分模式的宣告變數會遮蔽 match
結構外同名的變數,和所有變數一樣。在範例 18-11 中,我宣告了一個變數叫做 x
其有數值 Some(5)
和一個變數 y
其有數值 10
。然後我們建立一個數值 x
的 match
表達式。檢查配對分之中的模式並在最後用 println!
顯示出來,並嘗試在程式碼執行或進一步閱讀之前推測其會顯示的結果會為何。
檔案名稱:src/main.rs
fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!("取得 50"), Some(y) => println!("配對成功,y = {y}"), _ => println!("預設情形,x = {:?}", x), } println!("最後結果:x = {:?}, y = {y}", x); }
讓我們跑一遍看看當 match
執行時發生了什麼事。第一個配對分支並不符合 x
定義的數值,所以程式繼續執行下去。
第二個配對分支宣告了一個新的變數叫做 y
來配對 Some
內的任何數值。因為我們位於 match
表達式內的新作用域,此新的 y
變數並不是我們一開始宣告有數值 10 的 y
。這個新的 y
會配對 Some
內的任何數值,也就是 x
擁有的數值。因此,這個新的 y
會綁定 x
中 Some
的內部數值。該數值是 5
,所以該分支的表達式就會執行並印出 配對成功,y = 5
。
如果 x
是 None
數值而非 Some(5)
的話,前兩個分支的模式都不會配對到,所以數值會配對到底線的分支。我們沒有在底線分支的模式中宣告 x
變數,所以表達式中的 x
仍然是外部沒有被遮蔽的 x
。在這樣的假設狀況下,match
會印出 預設情形,x = None
。
當 match
完成時,其作用域就會結束,所以作用域內的內部 y
也會結束。最後一個 println!
會顯示 最後結果:x = Some(5), y = 10
。
要建立個能對外部 x
與 y
數值做比較的 match
表達式而非遮蔽變數的話,我們需要改用條件配對守護。我們會在之後的「提供額外條件的配對守護」段落討論配對守護。
多重模式
在 match
表達式中,你可以使用 |
語法來配對數個模式,這是 OR(或) 運算子模式。舉例來說,以下程式碼會配對 x
的數值到配對分支,第一個分支有個 OR 的選項,代表如果 x
的數值配對的到分支中任一數值的話,該分支的程式碼就會執行:
fn main() { let x = 1; match x { 1 | 2 => println!("一或二"), 3 => println!("三"), _ => println!("任意數字"), } }
此程式碼會印出 一或二
。
透過 ..=
配對數值範圍
..=
語法讓我們可以配對一個範圍內包含的數值。在以下程式碼中,當模式配對的到範圍內的任何數值時,該分支就會執行:
fn main() { let x = 5; match x { 1..=5 => println!("一到五"), _ => println!("其他"), } }
如果 x
是 1、2、3、4 或 5 的話,第一個分支就能配對到。在配對多重數值時,此語法比使用 |
運算子來表達相同概念還輕鬆得多。如果我們使用 |
的話,就得指明 1 | 2 | 3 | 4 | 5
而非 1..=5
。指定範圍相對就簡短許多,尤其是如果我們得配對像是數字 1 到 1,000 的話!
編譯器會在編譯時檢查範圍是否為空,然後因為char
與數字數值是 Rust 中唯一能判斷範圍是否為空的型別,所以範圍只允許使用數字或 char
數值。
以下是使用 char
數值作為範圍的範例:
fn main() { let x = 'c'; match x { 'a'..='j' => println!("前半部 ASCII 字母"), 'k'..='z' => println!("後半部 ASCII 字母"), _ => println!("其他"), } }
Rust 可以知道 'c'
有包含在第一個模式的範圍內,所以印出 前半部 ASCII 字母
。
解構拆開數值
我們可以使用模式來解構結構體、列舉與元組,以便使用這些數值的不同部分。讓我們依序來看看。
解構結構體
範例 18-12 有個結構體 Point
其有兩個欄位 x
與 y
,我們可以在 let
陳述式使用模式來拆開它。
檔案名稱:src/main.rs
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); }
此程式碼建立了變數 a
與 b
來配對 p
結構體中 x
與 y
的欄位。此範例顯示出模式中的變數名稱不必與結構體中的欄位名稱一樣。不過通常還是建議變數名稱與欄位名稱一樣,以便記得哪些變數來自於哪個欄位。因為用變數名稱來配對欄位是十分常見的,而且因為 let Point { x: x, y: y } = p;
會包含許多重複部分,所以配對結構體欄位的模式有另一種簡寫方式,你只需要列出結構體欄位的名稱,這樣從結構體建立的變數名稱就會有相同名稱。範例 18-13 顯示的程式碼行為與範例 18-12 一樣,但是在 let
模式建立的變數是 x
與 y
而非 a
與 b
。
檔案名稱:src/main.rs
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); }
此程式碼建立了變數 x
與 y
並配對到變數 p
的 x
與 y
欄位。結果就是變數 x
與 y
會包含 p
結構體中的數值。
我們也可以將字面值數值作為結構體模式中的一部分,而不用建立所有欄位的變數。這樣做我們可以在解構一些欄位成變數時,測試其他欄位是否有特定數值。
在範例 18-14 的 match
表達式將 Point
的數值分成三種情況:位於 x
軸的點(也就是 y = 0
)、位於 y
軸的點(x = 0
)或不在任何軸的點。
檔案名稱:src/main.rs
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; match p { Point { x, y: 0 } => println!("位於 x 軸的 {x}"), Point { x: 0, y } => println!("位於 y 軸的 {y}"), Point { x, y } => println!("不在任一軸:({x}, {y})"), } }
第一個分支透過指定 y
欄位配對字面值為 0
來配對任何在 x
軸上的點。此模式仍然會建立變數 x
能讓我們在此分支的程式碼中使用。
同樣地,第二個分支透過指定 x
欄位配對字面值為 0
來配對任何在 y
軸上的點,並建立擁有 y
欄位數值的變數 y
。第三個分支沒有指定任何字面值,所以它能配對任何其他 Point
並建立 x
與 y
欄位對應的變數。
在此例中,數值 p
會配對到第二個分支,因為其 x
為 0,所以此程式碼會印出 位於 y 軸的 7
。
回想一下 match
表達式在找到第一個符合的配對模式之後就會停止檢查分支,所以就算 Point { x: 0, y: 0}
真的在 x
軸與 y
軸,此程式碼也只會印出 位於 x 軸的 0
。
解構列舉
我們已經在本書中之前的章節就解構過列舉(比如第六章的範例 6-5)。但我們還沒談到的細節是解構列舉的模式必須與列舉定義中其所儲存的資料相符。作為示範,我們在範例 18-15 中使用範例 6-2 的 Message
列舉並寫一個 match
來提供會解構每個內部數值的模式。
檔案名稱:src/main.rs
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 => { println!("Quit 變體沒有資料能解構。"); } Message::Move { x, y } => { println!("Move 往 x 的方向為 {x} 且往 y 的方向為 {y}"); } Message::Write(text) => println!("文字訊息:{text}"), Message::ChangeColor(r, g, b) => { println!("變更顏色為紅色 {r}、綠色 {g} 與藍色 {b}") } } }
此程式碼會印出 變更顏色為紅色 0、綠色 160 與藍色 255
。請嘗試變更 msg
的數值來看看其他分支的程式碼會執行出什麼。
對於像是 Message::Quit
這種沒有任何資料的列舉,我們無法進一步解構出任何資料。我們只能配對其本身的數值 Message::Quit
,所以在該模式中沒有任何變數。
對於像是 Message::Move
這種類結構體列舉變體,我們可以使用類似於指定配對結構體的模式。在變體名稱之後,我們加上大括號以及列出欄位名稱作為變數,讓我們能拆成不同部分並在此分支的程式碼中使用。我們在此使用範例 18-13 一樣的簡寫形式。
對於像是 Message::Write
這種持有一個元素,以及 Message::ChangeColor
這種持有三個元素的類元組列舉變體,我們可以使用類似於配對元組的模式。模式中的變數數量必須與我們要配對的變體中元素個數相符。
解構巢狀結構體與列舉
到目前為止,我們所有的結構體或列舉配對範例的深度都只有一層,但配對也可以用於巢狀項目中!舉例來說,我們可以重構範例 18-15 的程式碼,在 ChangeColor
中支援 RGB 與 HSV 顏色,如範例 18-16 所示。
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!("變更顏色為紅色 {r}、綠色 {g} 與藍色 {b}"); } Message::ChangeColor(Color::Hsv(h, s, v)) => { println!("變更顏色為色相 {h}、飽和度 {s} 與明度 {v}"); } _ => (), } }
match
表達式的第一個分支模式會配對包含 Color::Rgb
變體的 Message::ChangeColor
列舉變體,然後該模式會綁定內部三個 i32
數值。第二個分支也是配對到 Message::ChangeColor
列舉變體,但是內部列舉會改配對 Color::Hsv
。我們可以在一個 match
表達式指定這些複雜條件,即使有兩個列舉參與其中。
解構結構體與元組
我們甚至可以用更複雜的方式來混合、配對並巢狀解構模式。以下範例展示了一個複雜的結構模式,其將一個結構體與一個元組置於另一個元組中,並將所有的原始數值全部解構出來:
fn main() { struct Point { x: i32, y: i32, } let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 }); }
此程式碼讓我們將複雜的型別拆成部分元件,讓我們可以分別使用我們有興趣的數值。
解構模式是個能方便使用部分數值的方式,比如結構體每個欄位分別獨立的數值。
忽略模式中的數值
你已經看過有時候在模式中忽略數值是很實用的,像是在 match
中的最後一個分支能捕獲所有剩餘用不到的可能數值。模式有一些方式可以忽略所有或部分數值:使用(你已經看過的) _
模式、在其他模式使用 _
模式、在名稱前加上底線,或是使用 ..
來忽略剩餘部分的數值。讓我們來探討如何與為何要使用這些模式吧。
透過 _
忽略整個數值
我們使用底線在來作為萬用字元(wildcard)模式,這會配對任何數值,但不會綁定其值。雖然底線 _
模式特別適合作為 match
表達式最後一個分支,但我們可以將它用在任何模式中,包含函式參數,如範例 18-17 所示。
檔案名稱:src/main.rs
fn foo(_: i32, y: i32) { println!("此程式碼只使用了參數 y:{}", y); } fn main() { foo(3, 4); }
此程式碼會完全忽略第一個引數傳入的數值 3
,並會印出 此程式碼只使用了參數 y:4
。
在大多數情況中如果當你不再需要特定函式參數的話,你會直接變更簽名讓它不會包含沒有使用到的參數。但忽略函式參數在某些場合會很有用。舉例來說,當你實作的特徵有個特定的型別簽名,但是你實作的函式本體不需要其中某個參數。這樣你就不會被編譯器警告沒有使用到的函式參數,會當做你有使用參數名稱一樣。
透過巢狀 _
忽略部分數值
我們也可以在其他模式中使用 _
來忽略部分數值。舉例來說,當我們只想測試部分數值,但不會用到執行的程式碼中其他部分數值的情況。範例 18-18 的程式碼負責管理設定值的數值。業務要求使用者不能覆寫已經存在的自訂數訂值,但可以取消設定值,也可以在目前未設定時提供數值。
fn main() { let mut setting_value = Some(5); let new_setting_value = Some(10); match (setting_value, new_setting_value) { (Some(_), Some(_)) => { println!("無法覆寫已經存在的自訂數值"); } _ => { setting_value = new_setting_value; } } println!("設定為 {:?}", setting_value); }
此程式碼會印出 無法覆寫已經存在的自訂數值
接著印出 設定為 Some(5)
。在第一個配對分支中,我們不需要去配對或使用任一 Some
變體內的數值,但我們的確需要檢測 setting_value
與 new_setting_value
是否都為 Some
變體的情況。在此情況下,我們印出為何不能變更 setting_value
,且不讓它被改變。
在其他所有情況下(無論是 setting_value
還是 new_setting_value
為 None
),我們用第二個分支的 _
模式來配對,我們讓 new_setting_value
變成 setting_value
。
我們也可以在同個模式中的多重位置使用底線來忽略特定數值。範例 18-19 忽略了有五個元素的元組中第二個與第四個數值。
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, _, third, _, fifth) => { println!("一些數字:{first}、{third}、{fifth}") } } }
此程式碼會印出 一些數字:2、8、32
,然後數值 4 與 16 會被忽略。
在名稱前加上 _
來忽略未使用的變數
如果你建立了一個變數但沒有在任何地方使用到它,Rust 通常會提出警告,因為未使用的變數可能會是個錯誤。但有時候先建立個你還沒有使用的變數是很有用的,像是你還在寫原型或是才剛開個專案而已。在這種場合,你可以在尚未使用的變數名稱前加上底線,來告訴 Rust 不用提出警告。在範例 18-20 中,我們建立了兩個未使用的變數,但當我們編譯此程式碼時,我們應該會只收到其中一個的警告而已。
檔案名稱:src/main.rs
fn main() { let _x = 5; let y = 10; }
我們在此收到沒有使用變數 y
的警告,但是我們沒有收到 _x
的警告。
注意到只使用 _
與在名稱前加上底線之間是有些差別的。_x
仍會綁定數值到變數中,但 _
不會做任何綁定。為了展示這樣的區別是有差的,我們用範例 18-21 來展示一個錯誤。
fn main() {
let s = Some(String::from("哈囉!"));
if let Some(_s) = s {
println!("發現字串");
}
println!("{:?}", s);
}
我們會收到錯誤,因為 s
的數值仍會被移至 _s
,讓我們無法再使用 s
。不過只使用底線的話就不會綁定數值。範例 18-22 就能夠編譯不會產生任何錯誤,因為 s
沒有移至 _
。
fn main() { let s = Some(String::from("哈囉!")); if let Some(_) = s { println!("發現字串"); } println!("{:?}", s); }
此程式碼就能執行,因為我們沒有將 s
綁定給誰,它沒被移動。
透過 ..
忽略剩餘部分數值
對於有許多部分的數值,我們可以用 ..
語法來使用特定部分,然後忽略剩餘部分,來避免需要對每個要忽略的數值都得加上底線。..
模式會忽略模式中剩餘尚未配對的任何部分數值。在範例 18-23 中,我們有個 Point
結構體存有三維空間中的座標。而在 match
表達式中,我們想要只處理 x
座標並忽略 y
與 z
欄位的數值。
fn main() { struct Point { x: i32, y: i32, z: i32, } let origin = Point { x: 0, y: 0, z: 0 }; match origin { Point { x, .. } => println!("x 為 {}", x), } }
我們列出 x
數值接著只包含 ..
模式。這比需要列出 y: _
和 z: _
還要快,尤其是當我們要處理有許多欄位的結構體,但只需要用到一或兩個欄位的狀況下。
..
語法會擴展其所有所需得數值。範例 18-24 展示如何在元組使用 ..
。
檔案名稱:src/main.rs
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, .., last) => { println!("一些數字:{first}、{last}"); } } }
在此程式碼中,第一個與最後一個數值會配對到 first
與 last
。..
會配對並忽略中間所有數值。
然而,使用 ..
必須是明確的。如果 Rust 無法確定是哪些數值要配對,而哪些是要忽略的話,它會回傳錯誤給我們。範例 18-25 含糊地使用了 ..
,所以它無法編譯。
檔案名稱:src/main.rs
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("一些數字:{}", second)
},
}
}
當我們編譯此範例時,我們會得到此錯誤:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` due to previous error
Rust 不可能會知道在配對 second
之前要忽略多少元組中的數值,以及在之後得再忽略多少數值此程式碼可以代表我們想要忽略 2
、綁定 second
到 4
然後忽略 8
、16
和 32
;或者我們想要忽略 2
和 4
、綁定 second
到 8
然後忽略 16
和 32
,以及更多可能。變數名稱 second
對 Rust 沒有任何特別意義,所以我們得到編譯錯誤,因為像這樣在兩個地方使用 ..
是含糊不清的。
提供額外條件的配對守護
配對守護(match guard)是個在 match
分支之後額外指定的 if
條件,此條件也必須符合配對才能選擇該分支。配對守護適用於比單獨模式所能表達的還複雜的情況。
該條件能使用配對建立的變數。範例 18-26 展示 match
的第一個分支有個模式 Some(x)
並使用配對守護 if x % 2 == 0
(如果數字為偶數就會成立)。
fn main() { let num = Some(4); match num { Some(x) if x % 2 == 0 => println!("數字 {x} 是偶數"), Some(x) => println!("數字 {x} 是奇數"), None => (), } }
此範例會印出 數字 4 是偶數
。當 num
與第一個分支做比較時,它會配對到,因為 Some(4)
能與 Some(x)
做配對。然後配對守護會檢查數值 x
除以 2 的餘數是否為 0,然後因為的確如此,所以就選擇了第一個分支。
如果 num
為 Some(5)
的話,第一個分支的配對守護則會是否,因為 5 除以 2 的餘數為 1,並不等於 0。Rust 就會接著檢查第二條分支,然後因為第二條分支沒有任何配對守護所以能配對到任何 Some
變體。
在模式中沒有任何方式能夠表達 if x % 2 == 0
,所以配對守護讓我們能夠有能力表達此邏輯。這種額外的表達能力的缺點是,編譯器對有配對守護的分支就不會窮舉檢查。
在範例 18-11 中,我們提到我們可以使用模式配對來解決我們的模式遮蔽問題。回想一下 match
表達式中使用的是模式內建立的新變數,而不是使用 match
外部的變數。該新變數會讓我們無法測試外部變數的數值。範例 18-27 展示我們如何使用配對守護來修正此問題。
檔案名稱:src/main.rs
fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!("取得 50"), Some(n) if n == y => println!("配對成功,n = {n}"), _ => println!("預設情形,x = {:?}", x), } println!("最後結果:x = {:?}, y = {y}", x); }
此程式碼現在會印出 預設情形,x = Some(5)
。第二個模式中沒有宣告新的變數 y
來遮蔽外部的 y
,意味著我們可以在配對守護中使用外部的 y
。我們不再指定模式為 Some(y)
,因為這樣會遮蔽外部的 y
,我們改指定成 Some(n)
。這樣建立了一個新的變數 n
且不會遮蔽任何事物,因為 match
外部沒有任何變數 n
。
配對守護 if n == y
不屬於模式,因此不會宣告任何新變數。此 y
就是外部的 y
而非新遮蔽的 y
,而且我們可以透過將 n
與 y
做比較來檢查數值是否與外部 y
的數值相等。
你也可以在配對守護中使用 OR 運算子 |
來指定多重模式,配對守護的條件會套用在所有的模式中。範例 18-28 顯示了結合配對守護與使用 |
模式之間的優先層級(precedence)。此例中的重點部分在於 if y
配對守護能套用在 4
、5
與 6
,而不是只有 6
會用到 if y
。
fn main() { let x = 4; let y = false; match x { 4 | 5 | 6 if y => println!("是"), _ => println!("否"), } }
此配對條件表示該分支只有在數值 x
等於 4
、5
或 6
,以及如果 y
為 true
時才算配對到。當此程式碼執行時,第一個分支的模式有配對到,因為 x
為 4
,但是配對守護 if y
為否,所以不會選擇第一個分支。程式碼會移動到第二個分支,然後程式會配對到並印出 no
。原因在於 if
條件會套用到整個模式 4 | 5 | 6
,而不是只有最後一個數值 6
。換句話說,配對守護與模式之間的優先層級會像是這樣:
(4 | 5 | 6) if y => ...
而不是這樣:
4 | 5 | (6 if y) => ...
在執行此程式碼之後,優先層級的行為就很明顯了,如果配對守護只會用在由 |
運算子指定數值列表中最後一個數值的話,該分支應該要能配對到並讓程式印出 是
。
@
綁定
At 運算子(@
)能讓我們在測試某個數值是否配對的到一個模式的同時,建立出一個變數來持有該數值。在範例 18-29 我們想要測試 Message::Hello
的 id
欄位是否位於 3..=7
的範圍中。我們也想要將該數值綁定到變數 id_variable
之中,讓我們可以在該分支對應的程式碼中使用它。我們可以將此變數命名為與欄位同名的 id
,但在此例中我們會使用不同名稱。
fn main() { enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id_variable @ 3..=7, } => println!("id 在此範圍中:{}", id_variable), Message::Hello { id: 10..=12 } => { println!("id 在其他範圍中") } Message::Hello { id } => println!("找到其他 id:{}", id), } }
此範例會印出 id 在此範圍中:5
。透過在範圍 3..=7
之前指定 id_variable @
,我們能獲取要配對到範圍的數值,並同時測試該數值是否有配對到範圍模式。
在第二個分支中,我們只有在模式中指定範圍,該分支對應的程式碼就沒有變數能包含 id
欄位的實際數值。id
欄位數值可能是 10、11 或 12,但此模式的程式碼不會知道其值為何。該模式程式碼無法使用 id
欄位的數值,因為我們沒有將 id
數值存為變數。
在最後一個分支中,我們指定沒有限制範圍的變數,我們有能在分支程式碼中使用的有效變數 id
。原因是因為我們使用了結構體欄位簡寫語法。不過我們在此分支沒有像前兩個分支進行任何對 id
欄位的測試,任何數值都會配對到此模式。
使用 @
讓我們能在一個模式中測試數值並將其儲存至變數。
總結
Rust 的模式對於分辨不同種資料來說非常實用。當在 match
表達式中使用時,Rust 確保你的模式有涵蓋所有可能數值,不然你的程式就不會編譯通過。在 let
陳述式與函式參數中的模式使它們的結構更實用,在能夠解構數值成更小部分的同時賦值給變數。我們能夠建立簡單或複雜的模式來滿足我們的需求。
接下來,在本書的倒數第二章中,我們要來看 Rust 眾多特色中的一些進階部分。