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

ตำแหน่งทั้งหมดที่สามารถใช้รูปแบบได้

รูปแบบ (Patterns) ปรากฏขึ้นในหลายๆ ตำแหน่งในภาษา Rust และคุณได้ใช้งานพวกมันมามากมายโดยไม่รู้ตัว! ในหัวข้อนี้จะอธิบายถึงตำแหน่งทั้งหมดที่รูปแบบสามารถใช้งานได้อย่างถูกต้อง

แขนของ match (Arms)

ดังที่ได้อภิปรายไว้ในบทที่ 6 เราใช้รูปแบบในแขน (arms) ของพจน์ match โดยหลักการอย่างเป็นทางการ พจน์ match นิยามด้วยคีย์เวิร์ด match ตามด้วยค่าที่จะนำมาจับคู่ และแขนจับคู่ตั้งแต่หนึ่งแขนขึ้นไป ซึ่งประกอบด้วยรูปแบบและพจน์ที่จะทำงานหากค่านั้นจับคู่ตรงกับรูปแบบของแขนนั้น เช่นนี้:

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

ตัวอย่างเช่น นี่คือพจน์ match จากโค้ดตัวอย่างที่ 6-5 ที่จับคู่กับค่า Option<i32> ในตัวแปร x:

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

รูปแบบในพจน์ match นี้คือ None และ Some(i) ทางด้านซ้ายของแต่ละเครื่องหมายลูกศร

ข้อกำหนดประการหนึ่งสำหรับพจน์ match คือพวกมันจำเป็นต้องมีความครอบคลุมทุกกรณี (exhaustive) ในแง่ที่ว่าความเป็นไปได้ทั้งหมดสำหรับค่าในพจน์ match จะต้องถูกนับรวมไว้ วิธีหนึ่งในการทำให้แน่ใจว่าคุณได้ครอบคลุมทุกความเป็นไปได้คือการมีรูปแบบรวบตึง (catch-all pattern) สำหรับแขนสุดท้าย: ตัวอย่างเช่น ชื่อตัวแปรที่จับคู่กับค่าใดก็ได้จะไม่มีวันล้มเหลว จึงครอบคลุมทุกกรณีที่เหลืออยู่ได้

รูปแบบเฉพาะอย่าง _ จะจับคู่กับอะไรก็ได้ แต่มันไม่มีวันผูก (bind) เข้ากับตัวแปร ดังนั้นมันจึงมักถูกใช้ในแขนจับคู่สุดท้าย รูปแบบ _ มีประโยชน์เมื่อคุณต้องการละเว้นค่าใดๆ ที่ไม่ได้ระบุไว้ ตัวอย่างเช่น เราจะครอบคลุมรูปแบบ _ โดยละเอียดเพิ่มเติมในส่วน “Ignoring Values in a Pattern” ในบทนี้ในภายหลัง

คำสั่ง let

ก่อนหน้าบทนี้ เราได้อภิปรายอย่างชัดแจ้งเฉพาะการใช้รูปแบบกับ match และ if let เท่านั้น แต่ในความเป็นจริง เราได้ใช้รูปแบบในตำแหน่งอื่นด้วย รวมถึงในคำสั่ง let ตัวอย่างเช่น พิจารณาการกำหนดค่าตัวแปรอย่างตรงไปตรงมาด้วย let:

#![allow(unused)]
fn main() {
let x = 5;
}

ทุกครั้งที่คุณใช้คำสั่ง let แบบนี้ คุณได้ใช้รูปแบบมาโดยตลอด แม้ว่าคุณอาจจะไม่เคยตระหนักถึงมันก็ตาม! โดยหลักการอย่างเป็นทางการ คำสั่ง let มีลักษณะดังนี้:

let PATTERN = EXPRESSION;

ในคำสั่งเช่น let x = 5; ที่มีชื่อตัวแปรอยู่ในช่อง PATTERN ตัวชื่อตัวแปรนั้นเป็นเพียงรูปแบบในรูปอย่างง่ายแบบหนึ่งเท่านั้น Rust จะเปรียบเทียบพจน์กับรูปแบบและกำหนดชื่อใดก็ตามที่มันพบ ดังนั้น ในตัวอย่าง let x = 5; ตัว x คือรูปแบบที่มีความหมายว่า “ผูกสิ่งที่จับคู่ตรงนี้เข้ากับตัวแปร x” เนื่องจากชื่อ x เป็นรูปแบบทั้งหมด รูปแบบนี้จึงหมายถึง “ผูกทุกอย่างเข้ากับตัวแปร x ไม่ว่าค่านั้นจะเป็นอะไรก็ตาม”

เพื่อให้เห็นแง่มุมของการจับคู่รูปแบบของ let ชัดเจนยิ่งขึ้น พิจารณาโค้ดตัวอย่างที่ 19-1 ซึ่งใช้รูปแบบกับ let เพื่อแยกโครงสร้าง (destructure) tuple

fn main() {
    let (x, y, z) = (1, 2, 3);
}

ในที่นี้ เราจับคู่ tuple กับรูปแบบ Rust จะเปรียบเทียบค่า (1, 2, 3) กับรูปแบบ (x, y, z) และเห็นว่าค่านั้นจับคู่ตรงกับรูปแบบ—นั่นคือ มันเห็นว่าจำนวนองค์ประกอบเท่ากันทั้งสองฝั่ง—ดังนั้น Rust จึงผูก 1 เข้ากับ x, 2 เข้ากับ y, และ 3 เข้ากับ z คุณสามารถคิดว่ารูปแบบ tuple นี้เป็นการซ้อนรูปแบบตัวแปรเดี่ยวสามตัวไว้ภายในมัน

หากจำนวนองค์ประกอบในรูปแบบไม่ตรงกับจำนวนองค์ประกอบใน tuple ชนิดข้อมูลโดยรวมจะไม่ตรงกันและเราจะได้รับข้อผิดพลาดจากคอมไพเลอร์ ตัวอย่างเช่น โค้ดตัวอย่างที่ 19-2 แสดงความพยายามแยกโครงสร้าง tuple ที่มีสามองค์ประกอบออกเป็นตัวแปรสองตัว ซึ่งจะไม่สามารถทำงานได้

fn main() {
    let (x, y) = (1, 2, 3);
}

การพยายามคอมไพล์โค้ดนี้จะส่งผลให้เกิดข้อผิดพลาดทางชนิดข้อมูลดังนี้:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`
  |         |
  |         expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`

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

ในการแก้ไขข้อผิดพลาด เราสามารถละเว้นค่าหนึ่งค่าหรือหลายค่าใน tuple ได้โดยใช้ _ หรือ .. ดังที่คุณจะได้เห็นในส่วน “Ignoring Values in a Pattern” หากปัญหาคือเรามีตัวแปรในรูปแบบมากเกินไป ทางแก้คือการทำให้ชนิดข้อมูลตรงกันโดยการลบตัวแปรออก เพื่อให้จำนวนตัวแปรเท่ากับจำนวนองค์ประกอบใน tuple

พจน์เงื่อนไข if let

ในบทที่ 6 เราได้อภิปรายวิธีใช้พจน์ if let เป็นหลักในฐานะวิธีสั้นกว่าในการเขียนสิ่งที่เทียบเท่ากับ match ที่จับคู่เฉพาะกรณีเดียว ซึ่งตัวเลือกเสริมคือ if let สามารถมี else ที่สอดคล้องกันซึ่งบรรจุโค้ดที่จะทำงานหากรูปแบบใน if let ไม่จับคู่ตรงกัน

โค้ดตัวอย่างที่ 19-3 แสดงให้เห็นว่าเป็นไปได้เช่นกันที่จะผสมผสานพจน์ if let, else if, และ else if let การทำเช่นนั้นให้ความยืดหยุ่นแก่เรามากกว่าพจน์ match ซึ่งเราสามารถแสดงค่าได้เพียงค่าเดียวเพื่อเปรียบเทียบกับรูปแบบ นอกจากนี้ Rust ไม่ได้กำหนดให้เงื่อนไขในลำดับของแขน if let, else if, และ else if let ต้องเกี่ยวข้องกัน

โค้ดในโค้ดตัวอย่างที่ 19-3 ทำการกำหนดว่าควรใช้สีใดทำเป็นพื้นหลังตามลำดับการตรวจสอบเงื่อนไขหลายประการ สำหรับตัวอย่างนี้ เราได้สร้างตัวแปรที่มีค่าที่กำหนดไว้ตายตัว (hardcoded values) ซึ่งในโปรแกรมจริงอาจได้รับมาจากอินพุตของผู้ใช้

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, {color}, as the background");
    } 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");
    }
}

หากผู้ใช้ระบุสีโปรด สีก็นั้นจะถูกใช้เป็นพื้นหลัง หากไม่ได้ระบุสีโปรดและวันนี้เป็นวันอังคาร สีพื้นหลังจะเป็นสีเขียว มิฉะนั้น หากผู้ใช้ระบุอายุเป็นสตริงและเราสามารถแปลง (parse) เป็นตัวเลขได้สำเร็จ สีกจะเป็นสีม่วงหรือสีส้มขึ้นอยู่กับค่าของตัวเลข หากไม่มีเงื่อนไขใดในเหล่านี้ตรง สีพื้นหลังจะเป็นสีน้ำเงิน

โครงสร้างเงื่อนไขนี้ช่วยให้เรารองรับความต้องการที่ซับซ้อนได้ ด้วยค่าที่กำหนดไว้อย่างตายตัวที่เรามีตรงนี้ ตัวอย่างนี้จะพิมพ์ Using purple as the background color

คุณจะเห็นว่า if let สามารถแนะนำตัวแปรใหม่ที่มาบังตัวแปรที่มีอยู่เดิม (shadowing) ในลักษณะเดียวกับที่แขนของ match ทำได้: บรรทัด if let Ok(age) = age แนะนำตัวแปร age ตัวใหม่ที่บรรจุค่าภายในแวเรียนต์ Ok ซึ่งจะบดบังตัวแปร age ที่มีอยู่เดิม นั่นหมายความว่าเราจำเป็นต้องวางเงื่อนไข if age > 30 ไว้ภายในบล็อกนั้น: เราไม่สามารถรวมสองเงื่อนไขนี้เข้าด้วยกันเป็น if let Ok(age) = age && age > 30 ได้ ตัวแปร age ใหม่ที่เราต้องการนำมาเปรียบเทียบกับ 30 จะยังไม่มีผลจนกว่าขอบเขต (scope) ใหม่จะเริ่มต้นขึ้นด้วยวงเล็บปีกกา

ข้อเสียของการใช้พจน์ if let คือคอมไพเลอร์จะไม่ตรวจสอบความครอบคลุมทุกกรณี (exhaustiveness) ในขณะที่พจน์ match จะตรวจสอบ หากเราละเว้นบล็อก else สุดท้ายไป และทำให้พลาดการจัดการบางกรณี คอมไพเลอร์จะไม่แจ้งเตือนเราถึงบั๊กทางตรรกะที่อาจเกิดขึ้นได้

ลูปเงื่อนไข while let

มีโครงสร้างคล้ายกับ if let ลูปเงื่อนไข while let อนุญาตให้ลูป while ทำงานตราบใดที่รูปแบบยังคงจับคู่ตรงอย่างต่อเนื่อง ในโค้ดตัวอย่างที่ 19-4 เราแสดงลูป while let ที่รอคอยข้อความที่ส่งระหว่าง threads แต่ในกรณีนี้เป็นการตรวจสอบ Result แทนที่จะเป็น Option

fn main() {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        for val in [1, 2, 3] {
            tx.send(val).unwrap();
        }
    });

    while let Ok(value) = rx.recv() {
        println!("{value}");
    }
}

ตัวอย่างนี้จะพิมพ์ 1, 2, และ 3 เมธอด recv ดึงข้อความแรกออกจากฝั่งรับข้อความของ channel และคืนค่า Ok(value) เมื่อตอนที่เราเห็น recv ครั้งแรกในบทที่ 16 เราทำการ unwrap ข้อผิดพลาดโดยตรง หรือมีปฏิสัมพันธ์กับมันในฐานะ iterator โดยใช้ลูป for อย่างไรก็ตาม ดังที่โค้ดตัวอย่างที่ 19-4 แสดง เรายังสามารถใช้ while let ได้ด้วย เพราะเมธอด recv คืนค่า Ok ทุกครั้งที่มีข้อความมาถึง ตราบใดที่ฝั่งส่งยังมีตัวตนอยู่ และจะสร้าง Err ออกมาเมื่อฝั่งส่งตัดการเชื่อมต่อ

ลูป for

ในลูป for ค่าที่ตามหลังคีย์เวิร์ด for โดยตรงคือรูปแบบ ตัวอย่างเช่น ใน for x in y ตัว x คือรูปแบบ โค้ดตัวอย่างที่ 19-5 สาธิตวิธีใช้รูปแบบในลูป for เพื่อแยกโครงสร้าง (destructure) หรือแยกชิ้นส่วน tuple ซึ่งเป็นส่วนหนึ่งของลูป for

fn main() {
    let v = vec!['a', 'b', 'c'];

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

โค้ดในโค้ดตัวอย่างที่ 19-5 จะพิมพ์สิ่งต่อไปนี้:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

เราดัดแปลง iterator โดยใช้เมธอด enumerate เพื่อให้มันสร้างค่าและดัชนี (index) สำหรับค่านั้น โดยจัดวางไว้ใน tuple ค่าแรกที่ถูกสร้างขึ้นคือ tuple (0, 'a') เมื่อค่านี้นำมาจับคู่กับรูปแบบ (index, value) ตัว index จะเป็น 0 และ value จะเป็น 'a' ซึ่งจะพิมพ์บรรทัดแรกของผลลัพธ์ออกมา

พารามิเตอร์ของฟังก์ชัน

พารามิเตอร์ของฟังก์ชันสามารถเป็นรูปแบบได้เช่นกัน โค้ดในโค้ดตัวอย่างที่ 19-6 ซึ่งประกาศฟังก์ชันชื่อ foo ที่รับพารามิเตอร์หนึ่งตัวชื่อ x ชนิดข้อมูล i32 ควรจะดูคุ้นเคยในตอนนี้

fn foo(x: i32) {
    // code goes here
}

fn main() {}

ส่วน x คือรูปแบบ! เช่นเดียวกับที่เราทำกับ let เราสามารถจับคู่ tuple ในอาร์กิวเมนต์ของฟังก์ชันกับรูปแบบได้ โค้ดตัวอย่างที่ 19-7 แยกค่าใน tuple เมื่อเราส่งมันไปยังฟังก์ชัน

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

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

โค้ดนี้จะพิมพ์ Current location: (3, 5) ค่า &(3, 5) จับคู่ตรงกับรูปแบบ &(x, y) ดังนั้น x จึงมีค่าเป็น 3 และ y จึงมีค่าเป็น 5

เรายังสามารถใช้รูปแบบในรายการพารามิเตอร์ของโคลเชอร์ (closure) ในลักษณะเดียวกันกับในรายการพารามิเตอร์ของฟังก์ชันได้ เนื่องจากโคลเชอร์คล้ายคลึงกับฟังก์ชัน ดังที่อภิปรายในบทที่ 13

ณ จุดนี้ คุณได้เห็นวิธีใช้รูปแบบหลากหลายวิธีแล้ว แต่รูปแบบไม่ได้ทำงานเหมือนกันในทุกตำแหน่งที่เราสามารถใช้มันได้ ในบางตำแหน่ง รูปแบบต้องเป็นแบบ irrefutable (หักล้างไม่ได้) ในสถานการณ์อื่น สามารถเป็นแบบ refutable (หักล้างได้) เราจะอภิปรายสองแนวคิดนี้ต่อไป