Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

โครงสร้าง Control Flow match

Rust มีโครงสร้าง control flow ที่ทรงพลังมากชื่อ match ที่ให้คุณเปรียบ เทียบค่ากับชุดของ pattern แล้ว execute โค้ดตามว่า pattern ไหน match Pattern ประกอบจากค่า literal, ชื่อตัวแปร, wildcard และอื่น ๆ อีกมาก บทที่ 19 ครอบคลุม pattern ชนิดต่าง ๆ ทั้งหมดและสิ่งที่พวกมันทำ พลังของ match มาจากความสามารถในการแสดงออกของ pattern และข้อเท็จจริงที่ว่า compiler ยืนยันว่าทุกกรณีที่เป็นไปได้ถูก จัดการ

คิดถึง match expression เหมือนเครื่องคัดแยกเหรียญ — เหรียญไหลลงรางที่มีรู ขนาดต่างกัน และแต่ละเหรียญตกผ่านรูแรกที่เจอที่มันลงพอดี ในแบบเดียวกัน ค่าผ่านแต่ละ pattern ใน match และที่ pattern แรกที่ค่า “พอดี” ค่าตกลงใน block โค้ดที่ผูกอยู่เพื่อถูกใช้ระหว่างการ execute

พูดถึงเหรียญ มาใช้พวกมันเป็นตัวอย่างกับ match! เราเขียนฟังก์ชันที่รับ เหรียญสหรัฐที่ไม่รู้จัก และในแบบคล้ายกับเครื่องคัดแยก กำหนดว่าเป็นเหรียญ อะไร แล้ว return ค่าเป็นเซ็นต์ ดังที่แสดงใน Listing 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,
    }
}

fn main() {}
Listing 6-3: enum และ match expression ที่มี variant ของ enum เป็น pattern

มาแยก match ในฟังก์ชัน value_in_cents ก่อน เราใส่ keyword match ตาม ด้วย expression ซึ่งในกรณีนี้คือค่า coin ดูคล้าย conditional expression ที่ใช้กับ if แต่มีความแตกต่างใหญ่ — กับ if condition ต้องประเมินเป็น ค่า Boolean แต่ที่นี่เป็น type ใดก็ได้ Type ของ coin ในตัวอย่างนี้คือ enum Coin ที่เราประกาศในบรรทัดแรก

ถัดไปคือ arm ของ match arm มีสองส่วน — pattern และโค้ดบางตัว arm แรก ที่นี่มี pattern ที่เป็นค่า Coin::Penny แล้ว operator => ที่คั่น pattern และโค้ดที่จะรัน โค้ดในกรณีนี้คือแค่ค่า 1 แต่ละ arm ถูกคั่นจาก ตัวถัดไปด้วย comma

เมื่อ match expression execute มันเปรียบเทียบค่าผลลัพธ์กับ pattern ของ แต่ละ arm ตามลำดับ ถ้า pattern match ค่า โค้ดที่ผูกกับ pattern นั้นถูก execute ถ้า pattern นั้นไม่ match ค่า การ execute ดำเนินต่อไปยัง arm ถัด ไป เหมือนในเครื่องคัดแยกเหรียญ เรามี arm ได้มากเท่าที่ต้องการ — ใน Listing 6-3 match ของเรามีสี่ arm

โค้ดที่ผูกกับแต่ละ arm เป็น expression และค่าผลลัพธ์ของ expression ใน arm ที่ match คือค่าที่ถูก return สำหรับทั้ง match expression

โดยทั่วไปเราไม่ใช้ curly bracket ถ้าโค้ดของ match arm สั้น เหมือนใน Listing 6-3 ที่แต่ละ arm แค่ return ค่า ถ้าคุณอยากรันโค้ดหลายบรรทัดใน match arm คุณต้องใช้ curly bracket และ comma ที่ตาม arm จึงเป็น optional เช่น โค้ดต่อไปนี้พิมพ์ “Lucky penny!” ทุกครั้งที่เมธอดถูกเรียกด้วย Coin::Penny แต่ยัง return ค่าสุดท้ายของ block คือ 1:

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

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

fn main() {}

Pattern ที่ Bind กับค่า

ฟีเจอร์ที่มีประโยชน์อีกอย่างของ match arm คือพวกมัน bind กับส่วนของค่าที่ match pattern ได้ นี่คือวิธีที่เราดึงค่าออกจาก variant ของ enum

เป็นตัวอย่าง ลองเปลี่ยน variant หนึ่งของ enum เพื่อเก็บข้อมูลภายใน ตั้งแต่ ปี 1999 ถึง 2008 สหรัฐผลิตเหรียญ quarter ที่มีการออกแบบต่างกันสำหรับ แต่ละรัฐ 50 รัฐที่ด้านหนึ่ง เหรียญอื่นไม่มีการออกแบบรัฐ เฉพาะเหรียญ quarter มีค่าเพิ่มเติมนี้ เราเพิ่มข้อมูลนี้เข้า enum ของเราได้ โดย เปลี่ยน variant Quarter ให้รวมค่า UsState ที่เก็บภายใน ซึ่งเราทำใน Listing 6-4

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

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

fn main() {}
Listing 6-4: enum Coin ที่ variant Quarter เก็บค่า UsState ด้วย

ลองจินตนาการว่าเพื่อนกำลังพยายามรวบรวมเหรียญ quarter ของรัฐทั้ง 50 รัฐ ขณะที่เราคัดเศษเหรียญตามชนิด เราจะพูดชื่อของรัฐที่ผูกกับแต่ละ quarter ด้วย เพื่อถ้าเป็นตัวที่เพื่อนยังไม่มี เขาจะเพิ่มเข้าสะสมของเขาได้

ใน match expression สำหรับโค้ดนี้ เราเพิ่มตัวแปรชื่อ state ใน pattern ที่ match ค่าของ variant Coin::Quarter เมื่อ Coin::Quarter match ตัว แปร state จะ bind กับค่ารัฐของ quarter นั้น จากนั้นเราใช้ state ใน โค้ดของ arm นั้นได้ ดังนี้:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

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

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
        }
    }
}

fn main() {
    value_in_cents(Coin::Quarter(UsState::Alaska));
}

ถ้าเราเรียก value_in_cents(Coin::Quarter(UsState::Alaska)) coin จะ เป็น Coin::Quarter(UsState::Alaska) เมื่อเราเปรียบเทียบค่านั้นกับ match arm แต่ละตัว ไม่มีตัวไหน match จนกระทั่งเราถึง Coin::Quarter(state) ณ จุดนั้น binding ของ state จะเป็นค่า UsState::Alaska เราใช้ binding นั้นใน println! expression ได้ จึงดึงค่ารัฐภายในออกจาก variant Coin สำหรับ Quarter

match Pattern ของ Option<T>

ในส่วนก่อนหน้า เราอยากดึงค่า T ภายในออกจากกรณี Some เมื่อใช้ Option<T> เรายังจัดการ Option<T> โดยใช้ match ได้ เหมือนที่เราทำกับ enum Coin! แทนการเปรียบเทียบเหรียญ เราจะเปรียบเทียบ variant ของ Option<T> แต่วิธีที่ match expression ทำงานยังเหมือนเดิม

สมมติเราอยากเขียนฟังก์ชันที่รับ Option<i32> และ ถ้ามีค่าภายใน บวก 1 กับค่านั้น ถ้าไม่มีค่าภายใน ฟังก์ชันควร return ค่า None และไม่พยายามทำ operation ใด ๆ

ฟังก์ชันนี้เขียนง่ายมาก ขอบคุณ match และจะดูเหมือน Listing 6-5

fn main() {
    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);
}
Listing 6-5: ฟังก์ชันที่ใช้ match expression บน Option<i32>

มาตรวจสอบการ execute ครั้งแรกของ plus_one ในรายละเอียดมากขึ้น เมื่อเรา เรียก plus_one(five) ตัวแปร x ใน body ของ plus_one จะมีค่า Some(5) จากนั้นเราเปรียบเทียบกับแต่ละ match arm:

fn main() {
    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);
}

ค่า Some(5) ไม่ match pattern None เราจึงดำเนินต่อไปยัง arm ถัดไป:

fn main() {
    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);
}

Some(5) match Some(i) ไหม? match! เรามี variant เดียวกัน i bind กับค่าภายใน Some ดังนั้น i รับค่า 5 โค้ดใน match arm จากนั้น execute เราจึงบวก 1 กับค่าของ i และสร้างค่า Some ใหม่กับยอดรวม 6 ภายใน

ทีนี้พิจารณาการเรียก plus_one ครั้งที่สองใน Listing 6-5 ที่ x คือ None เราเข้า match และเปรียบเทียบกับ arm แรก:

fn main() {
    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);
}

มัน match! ไม่มีค่าให้บวก โปรแกรมจึงหยุดและ return ค่า None ทางขวาของ => เพราะ arm แรก match ไม่มี arm อื่นถูกเปรียบเทียบ

การรวม match และ enum มีประโยชน์ในหลายสถานการณ์ คุณจะเห็น pattern นี้ มากในโค้ด Rust — match กับ enum, bind ตัวแปรกับข้อมูลภายใน แล้ว execute โค้ดตามนั้น มันยากนิดหน่อยตอนแรก แต่เมื่อคุณคุ้นแล้ว คุณจะอยากให้มีในทุก ภาษา มันเป็นที่ชื่นชอบของ user อย่างต่อเนื่อง

Match ต้องครอบคลุมทุกกรณี

มีอีกแง่มุมของ match ที่เราต้องพูดถึง — pattern ของ arm ต้องครอบคลุม ความเป็นไปได้ทั้งหมด พิจารณา version นี้ของฟังก์ชัน plus_one ของเรา ซึ่งมี bug และจะ compile ไม่ผ่าน:

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

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

เราไม่ได้จัดการกรณี None โค้ดนี้จึงทำให้เกิด bug โชคดี มันเป็น bug ที่ Rust รู้วิธีจับ ถ้าเราลอง compile โค้ดนี้ เราจะได้ error นี้:

$ 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
  |
note: `Option<i32>` defined here
 --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
 ::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
  |
  = note: not covered
  = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
  |
4 ~             Some(i) => Some(i + 1),
5 ~             None => todo!(),
  |

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

Rust รู้ว่าเราไม่ได้ครอบคลุมทุกกรณีที่เป็นไปได้ และยังรู้ด้วยว่าเราลืม pattern ไหน! Match ใน Rust เป็น exhaustive — เราต้อง exhaust ทุก ความเป็นไปได้สุดท้าย เพื่อให้โค้ด valid โดยเฉพาะในกรณีของ Option<T> เมื่อ Rust ป้องกันเราจากการลืมจัดการกรณี None แบบ explicit มันปกป้องเราจากการ สมมติว่าเรามีค่าเมื่อเราอาจมี null จึงทำให้ความผิดพลาดมูลค่าพันล้านดอลลาร์ ที่พูดถึงก่อนหน้าเป็นไปไม่ได้

Pattern จับทั้งหมดและ Placeholder _

ใช้ enum เรายังทำ action พิเศษสำหรับค่าเฉพาะบางตัวได้ แต่สำหรับค่าอื่น ทั้งหมดทำ action default หนึ่งตัว ลองนึกว่าเรากำลัง implement เกมที่ ถ้า คุณ roll dice ได้ 3 player ของคุณไม่เคลื่อนที่ แต่ได้หมวกใหม่หรูแทน ถ้า คุณ roll ได้ 7 player ของคุณเสียหมวกหรู สำหรับค่าอื่นทั้งหมด player ของคุณเคลื่อนที่จำนวนช่องนั้นบนกระดานเกม นี่คือ match ที่ implement logic นั้น ด้วยผลของการ roll dice ที่ hardcode แทนค่าสุ่ม และ logic อื่น ทั้งหมดแทนด้วยฟังก์ชันที่ไม่มี body เพราะการ implement พวกมันจริงอยู่นอก ขอบเขตของตัวอย่างนี้:

fn main() {
    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) {}
}

สำหรับ arm สองตัวแรก pattern คือค่า literal 3 และ 7 สำหรับ arm สุดท้าย ที่ครอบคลุมค่าอื่นที่เป็นไปได้ทั้งหมด pattern คือตัวแปรที่เราเลือกตั้งชื่อ other โค้ดที่รันสำหรับ arm other ใช้ตัวแปรโดยส่งให้ฟังก์ชัน move_player

โค้ดนี้ compile ผ่าน แม้เราจะไม่ได้ list ค่าที่เป็นไปได้ทั้งหมดที่ u8 มีได้ เพราะ pattern สุดท้ายจะ match ค่าทั้งหมดที่ไม่ได้ list เฉพาะ Pattern จับทั้งหมดนี้ตรงตามข้อกำหนดที่ match ต้อง exhaustive หมายเหตุว่า เราต้องวาง arm จับทั้งหมดท้ายสุด เพราะ pattern ถูกประเมินตามลำดับ ถ้าเรา วาง arm จับทั้งหมดก่อนหน้านี้ arm อื่นจะไม่ได้รัน Rust จะเตือนเราถ้าเรา เพิ่ม arm หลังจับทั้งหมด!

Rust ยังมี pattern ที่เราใช้ได้เมื่อเราอยากมีจับทั้งหมด แต่ไม่อยาก ใช้ ค่าใน pattern จับทั้งหมด — _ เป็น pattern พิเศษที่ match ค่าใด ๆ และไม่ bind กับค่านั้น สิ่งนี้บอก Rust ว่าเราจะไม่ใช้ค่า Rust จึงไม่เตือนเรา เกี่ยวกับตัวแปรที่ไม่ใช้

มาเปลี่ยนกฎของเกม — ตอนนี้ถ้าคุณ roll อะไรที่ไม่ใช่ 3 หรือ 7 คุณต้อง roll อีก เราไม่ต้องใช้ค่าจับทั้งหมดแล้ว เราจึงเปลี่ยนโค้ดให้ใช้ _ แทนตัวแปร ชื่อ other:

fn main() {
    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() {}
}

ตัวอย่างนี้ก็ตรงตามข้อกำหนด exhaustiveness ด้วย เพราะเราละเว้นค่าอื่น ทั้งหมดใน arm สุดท้ายแบบ explicit — เราไม่ได้ลืมอะไร

สุดท้าย เราจะเปลี่ยนกฎของเกมอีกครั้ง ให้ไม่มีอะไรอื่นเกิดในตาของคุณ ถ้า คุณ roll อะไรที่ไม่ใช่ 3 หรือ 7 เราแสดงสิ่งนั้นได้โดยใช้ค่า unit (empty tuple type ที่เราเอ่ยในส่วน “Tuple Type”) เป็น โค้ดที่ไปกับ arm _:

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

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

ที่นี่ เรากำลังบอก Rust แบบ explicit ว่าเราจะไม่ใช้ค่าอื่นใดที่ไม่ match pattern ใน arm ก่อนหน้า และเราไม่อยากรันโค้ดในกรณีนี้

มีอีกมากเกี่ยวกับ pattern และ matching ที่เราจะครอบคลุมใน บทที่ 19 ตอนนี้ เราจะไปต่อที่ syntax if let ซึ่งมีประโยชน์ในสถานการณ์ที่ match expression ยาวเกินไปนิด หน่อย