Rust match 控制流結(jié)構(gòu)

2023-03-22 15:09 更新
ch06-02-match.md
commit c76f1b4d011fe59fc4f5e6f258070fc40d9921e4

Rust 有一個(gè)叫做 match 的極為強(qiáng)大的控制流運(yùn)算符,它允許我們將一個(gè)值與一系列的模式相比較,并根據(jù)相匹配的模式執(zhí)行相應(yīng)代碼。模式可由字面值、變量、通配符和許多其他內(nèi)容構(gòu)成;第十八章會(huì)涉及到所有不同種類的模式以及它們的作用。match 的力量來源于模式的表現(xiàn)力以及編譯器檢查,它確保了所有可能的情況都得到處理。

可以把 match 表達(dá)式想象成某種硬幣分類器:硬幣滑入有著不同大小孔洞的軌道,每一個(gè)硬幣都會(huì)掉入符合它大小的孔洞。同樣地,值也會(huì)通過 match 的每一個(gè)模式,并且在遇到第一個(gè) “符合” 的模式時(shí),值會(huì)進(jìn)入相關(guān)聯(lián)的代碼塊并在執(zhí)行中被使用。

因?yàn)閯倓偺岬搅擞矌?,讓我們用它們來作為一個(gè)使用 match 的例子!我們可以編寫一個(gè)函數(shù)來獲取一個(gè)未知的硬幣,并以一種類似驗(yàn)鈔機(jī)的方式,確定它是何種硬幣并返回它的美分值,如示例 6-3 中所示。

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

示例 6-3:一個(gè)枚舉和一個(gè)以枚舉成員作為模式的 match 表達(dá)式

拆開 value_in_cents 函數(shù)中的 match 來看。首先,我們列出 match 關(guān)鍵字后跟一個(gè)表達(dá)式,在這個(gè)例子中是 coin 的值。這看起來非常像 if 使用的表達(dá)式,不過這里有一個(gè)非常大的區(qū)別:對(duì)于 if,表達(dá)式必須返回一個(gè)布爾值,而這里它可以是任何類型的。例子中的 coin 的類型是示例 6-3 中定義的 Coin 枚舉。

接下來是 match 的分支。一個(gè)分支有兩個(gè)部分:一個(gè)模式和一些代碼。第一個(gè)分支的模式是值 Coin::Penny 而之后的 => 運(yùn)算符將模式和將要運(yùn)行的代碼分開。這里的代碼就僅僅是值 1。每一個(gè)分支之間使用逗號(hào)分隔。

當(dāng) match 表達(dá)式執(zhí)行時(shí),它將結(jié)果值按順序與每一個(gè)分支的模式相比較。如果模式匹配了這個(gè)值,這個(gè)模式相關(guān)聯(lián)的代碼將被執(zhí)行。如果模式并不匹配這個(gè)值,將繼續(xù)執(zhí)行下一個(gè)分支,非常類似一個(gè)硬幣分類器??梢該碛腥我舛嗟姆种В菏纠?6-3 中的 match 有四個(gè)分支。

每個(gè)分支相關(guān)聯(lián)的代碼是一個(gè)表達(dá)式,而表達(dá)式的結(jié)果值將作為整個(gè) match 表達(dá)式的返回值。

如果分支代碼較短的話通常不使用大括號(hào),正如示例 6-3 中的每個(gè)分支都只是返回一個(gè)值。如果想要在分支中運(yùn)行多行代碼,可以使用大括號(hào),而分支后的逗號(hào)是可選的。例如,如下代碼在每次使用Coin::Penny 調(diào)用時(shí)都會(huì)打印出 “Lucky penny!”,同時(shí)仍然返回代碼塊最后的值,1

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

綁定值的模式

匹配分支的另一個(gè)有用的功能是可以綁定匹配的模式的部分值。這也就是如何從枚舉成員中提取值的。

作為一個(gè)例子,讓我們修改枚舉的一個(gè)成員來存放數(shù)據(jù)。1999 年到 2008 年間,美國(guó)在 25 美分的硬幣的一側(cè)為 50 個(gè)州的每一個(gè)都印刷了不同的設(shè)計(jì)。其他的硬幣都沒有這種區(qū)分州的設(shè)計(jì),所以只有這些 25 美分硬幣有特殊的價(jià)值??梢詫⑦@些信息加入我們的 enum,通過改變 Quarter 成員來包含一個(gè) State 值,示例 6-4 中完成了這些修改:

#[derive(Debug)] // 這樣可以立刻看到州的名稱
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

示例 6-4:Quarter 成員也存放了一個(gè) UsState 值的 Coin 枚舉

想象一下我們的一個(gè)朋友嘗試收集所有 50 個(gè)州的 25 美分硬幣。在根據(jù)硬幣類型分類零錢的同時(shí),也可以報(bào)告出每個(gè) 25 美分硬幣所對(duì)應(yīng)的州名稱,這樣如果我們的朋友沒有的話,他可以將其加入收藏。

在這些代碼的匹配表達(dá)式中,我們?cè)谄ヅ?nbsp;Coin::Quarter 成員的分支的模式中增加了一個(gè)叫做 state 的變量。當(dāng)匹配到 Coin::Quarter 時(shí),變量 state 將會(huì)綁定 25 美分硬幣所對(duì)應(yīng)州的值。接著在那個(gè)分支的代碼中使用 state,如下:

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        }
    }
}

如果調(diào)用 value_in_cents(Coin::Quarter(UsState::Alaska)),coin 將是 Coin::Quarter(UsState::Alaska)。當(dāng)將值與每個(gè)分支相比較時(shí),沒有分支會(huì)匹配,直到遇到 Coin::Quarter(state)。這時(shí),state 綁定的將會(huì)是值 UsState::Alaska。接著就可以在 println! 表達(dá)式中使用這個(gè)綁定了,像這樣就可以獲取 Coin 枚舉的 Quarter 成員中內(nèi)部的州的值。

匹配 Option<T>

我們?cè)谥暗牟糠种惺褂?nbsp;Option<T> 時(shí),是為了從 Some 中取出其內(nèi)部的 T 值;我們還可以像處理 Coin 枚舉那樣使用 match 處理 Option<T>!只不過這回比較的不再是硬幣,而是 Option<T> 的成員,但 match 表達(dá)式的工作方式保持不變。

比如我們想要編寫一個(gè)函數(shù),它獲取一個(gè) Option<i32> ,如果其中含有一個(gè)值,將其加一。如果其中沒有值,函數(shù)應(yīng)該返回 None 值,而不嘗試執(zhí)行任何操作。

得益于 match,編寫這個(gè)函數(shù)非常簡(jiǎn)單,它將看起來像示例 6-5 中這樣:

    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);

示例 6-5:一個(gè)在 Option<i32> 上使用 match 表達(dá)式的函數(shù)

匹配 Some(T)

讓我們更仔細(xì)地檢查 plus_one 的第一行操作。當(dāng)調(diào)用 plus_one(five) 時(shí),plus_one 函數(shù)體中的 x 將會(huì)是值 Some(5)。接著將其與每個(gè)分支比較。

            None => None,

值 Some(5) 并不匹配模式 None,所以繼續(xù)進(jìn)行下一個(gè)分支。

            Some(i) => Some(i + 1),

Some(5) 與 Some(i) 匹配嗎?當(dāng)然匹配!它們是相同的成員。i 綁定了 Some 中包含的值,所以 i 的值是 5。接著匹配分支的代碼被執(zhí)行,所以我們將 i 的值加一并返回一個(gè)含有值 6 的新 Some。

接著考慮下示例 6-5 中 plus_one 的第二個(gè)調(diào)用,這里 x 是 None。我們進(jìn)入 match 并與第一個(gè)分支相比較。

            None => None,

匹配上了!這里沒有值來加一,所以程序結(jié)束并返回 => 右側(cè)的值 None,因?yàn)榈谝粋€(gè)分支就匹配到了,其他的分支將不再比較。

將 match 與枚舉相結(jié)合在很多場(chǎng)景中都是有用的。你會(huì)在 Rust 代碼中看到很多這樣的模式:match 一個(gè)枚舉,綁定其中的值到一個(gè)變量,接著根據(jù)其值執(zhí)行代碼。這在一開始有點(diǎn)復(fù)雜,不過一旦習(xí)慣了,你會(huì)希望所有語(yǔ)言都擁有它!這一直是用戶的最愛。

匹配是窮盡的

match 還有另一方面需要討論:這些分支必須覆蓋了所有的可能性??紤]一下 plus_one 函數(shù)的這個(gè)版本,它有一個(gè) bug 并不能編譯:

    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

我們沒有處理 None 的情況,所以這些代碼會(huì)造成一個(gè) bug。幸運(yùn)的是,這是一個(gè) Rust 知道如何處理的 bug。如果嘗試編譯這段代碼,會(huì)得到這個(gè)錯(cuò)誤:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
   --> src/main.rs:3:15
    |
3   |         match x {
    |               ^ pattern `None` not covered
    |
    = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
    = note: the matched value is of type `Option<i32>`

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` due to previous error

Rust 知道我們沒有覆蓋所有可能的情況甚至知道哪些模式被忘記了!Rust 中的匹配是 窮盡的exhaustive):必須窮舉到最后的可能性來使代碼有效。特別的在這個(gè) Option<T> 的例子中,Rust 防止我們忘記明確的處理 None 的情況,這讓我們免于假設(shè)擁有一個(gè)實(shí)際上為空的值,從而使之前提到的價(jià)值億萬的錯(cuò)誤不可能發(fā)生。

通配模式和 _ 占位符

讓我們看一個(gè)例子,我們希望對(duì)一些特定的值采取特殊操作,而對(duì)其他的值采取默認(rèn)操作。想象我們正在玩一個(gè)游戲,如果你擲出骰子的值為 3,角色不會(huì)移動(dòng),而是會(huì)得到一頂新奇的帽子。如果你擲出了 7,你的角色將失去新奇的帽子。對(duì)于其他的數(shù)值,你的角色會(huì)在棋盤上移動(dòng)相應(yīng)的格子。這是一個(gè)實(shí)現(xiàn)了上述邏輯的 match,骰子的結(jié)果是硬編碼而不是一個(gè)隨機(jī)值,其他的邏輯部分使用了沒有函數(shù)體的函數(shù)來表示,實(shí)現(xiàn)它們超出了本例的范圍:

    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        other => move_player(other),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn move_player(num_spaces: u8) {}

對(duì)于前兩個(gè)分支,匹配模式是字面值 3 和 7,最后一個(gè)分支則涵蓋了所有其他可能的值,模式是我們命名為 other 的一個(gè)變量。other 分支的代碼通過將其傳遞給 move_player 函數(shù)來使用這個(gè)變量。

即使我們沒有列出 u8 所有可能的值,這段代碼依然能夠編譯,因?yàn)樽詈笠粋€(gè)模式將匹配所有未被特殊列出的值。這種通配模式滿足了 match 必須被窮盡的要求。請(qǐng)注意,我們必須將通配分支放在最后,因?yàn)槟J绞前错樞蚱ヅ涞?。如果我們?cè)谕ㄅ浞种Ш筇砑悠渌种В琑ust 將會(huì)警告我們,因?yàn)榇撕蟮姆种в肋h(yuǎn)不會(huì)被匹配到。

Rust 還提供了一個(gè)模式,當(dāng)我們不想使用通配模式獲取的值時(shí),請(qǐng)使用 _ ,這是一個(gè)特殊的模式,可以匹配任意值而不綁定到該值。這告訴 Rust 我們不會(huì)使用這個(gè)值,所以 Rust 也不會(huì)警告我們存在未使用的變量。

讓我們改變游戲規(guī)則:現(xiàn)在,當(dāng)你擲出的值不是 3 或 7 的時(shí)候,你必須再次擲出。這種情況下我們不需要使用這個(gè)值,所以我們改動(dòng)代碼使用 _ 來替代變量 other :

    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => reroll(),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn reroll() {}

這個(gè)例子也滿足窮舉性要求,因?yàn)槲覀冊(cè)谧詈笠粋€(gè)分支中明確地忽略了其他的值。我們沒有忘記處理任何東西。

最后,讓我們?cè)俅胃淖冇螒蛞?guī)則,如果你擲出 3 或 7 以外的值,你的回合將無事發(fā)生。我們可以使用單元值(在“元組類型”一節(jié)中提到的空元組)作為 _ 分支的代碼:

    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}

在這里,我們明確告訴 Rust 我們不會(huì)使用與前面模式不匹配的值,并且這種情況下我們不想運(yùn)行任何代碼。

我們將在第 18 章中介紹更多關(guān)于模式和匹配的內(nèi)容?,F(xiàn)在,讓我們繼續(xù)討論 if let 語(yǔ)法,這在 match 表達(dá)式有點(diǎn)啰嗦的情況下很有用。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)