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

ไวยากรณ์รูปแบบ (Pattern Syntax)

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

การจับคู่กับค่าตรงตัว (Matching Literals)

ดังที่คุณได้เห็นในบทที่ 6 คุณสามารถจับคู่รูปแบบกับค่าตรงตัว (literals) โดยตรงได้ โค้ดต่อไปนี้แสดงตัวอย่างบางส่วน:

fn main() {
    let x = 1;

    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

โค้ดนี้จะพิมพ์ one เพราะค่าใน x คือ 1 ไวยากรณ์นี้มีประโยชน์เมื่อคุณต้องการให้โค้ดของคุณดำเนินการบางอย่างหากมันได้รับค่ารูปธรรมเฉพาะเจาะจง

การจับคู่กับตัวแปรที่มีชื่อ (Matching Named Variables)

ตัวแปรที่มีชื่อ (Named variables) เป็นรูปแบบประเภท irrefutable ที่จับคู่ตรงกับค่าใดๆ ก็ได้ และเราได้ใช้พวกมันมาหลายครั้งในหนังสือเล่มนี้ อย่างไรก็ตาม มีความซับซ้อนบางประการเมื่อคุณใช้ตัวแปรที่มีชื่อในพจน์ match, if let, หรือ while let เนื่องจากพจน์ประเภทเหล่านี้แต่ละชนิดจะเริ่มต้นขอบเขต (scope) ใหม่ ตัวแปรที่ถูกประกาศเป็นส่วนหนึ่งของรูปแบบภายในพจน์เหล่านี้จึงจะบดบัง (shadow) ตัวแปรที่มีชื่อเดียวกันนอกโครงสร้าง เช่นเดียวกับกรณีของตัวแปรทั้งหมด ในโค้ดตัวอย่างที่ 19-11 เราประกาศตัวแปรชื่อ x มีค่า Some(5) และตัวแปร y มีค่า 10 จากนั้นเราสร้างพจน์ match บนค่า x ให้ดูรูปแบบในแขนของ match และ println! ตอนท้าย แล้วลองคิดว่าโค้ดจะพิมพ์อะไรออกมาก่อนที่จะรันโค้ดหรืออ่านต่อ

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {y}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}

เรามาไล่ดูสิ่งที่เกิดขึ้นเมื่อพจน์ match ทำงาน รูปแบบในแขนแรกไม่ตรงกับค่าของ x ที่ถูกกำหนดไว้ ดังนั้นโค้ดจึงทำงานต่อ

รูปแบบในแขนที่สองแนะนำตัวแปรใหม่ชื่อ y ที่จะจับคู่กับค่าใดๆ ก็ได้ภายในค่า Some เนื่องจากเราอยู่ในขอบเขตใหม่ภายในพจน์ match ตัวแปรนี้จึงเป็นตัวแปร y ตัวใหม่ ไม่ใช่ y ที่เราประกาศไว้ตอนแรกที่มีค่า 10 การผูก y ใหม่นี้จะจับคู่กับค่าใดก็ได้ภายใน Some ซึ่งก็คือสิ่งที่เรามีใน x ดังนั้น y ใหม่นี้จึงผูกเข้ากับค่าภายในของ Some ใน x ค่านั้นคือ 5 ดังนั้นพจน์สำหรับแขนนั้นจึงทำงานและพิมพ์ Matched, y = 5

หาก x เป็นค่า None แทนที่จะเป็น Some(5) รูปแบบในสองแขนแรกก็จะไม่จับคู่ตรงกัน ดังนั้นค่าจะไปจับคู่ตรงกับเครื่องหมายขีดล่าง (underscore) เราไม่ได้แนะนำตัวแปร x ในรูปแบบของแขนขีดล่าง ดังนั้น x ในพจน์นั้นจึงยังคงเป็น x ตัวนอกที่ไม่ถูกบดบัง ในกรณีสมมตินี้ match จะพิมพ์ Default case, x = None

เมื่อพจน์ match ทำงานเสร็จสิ้น ขอบเขตของมันก็จบลง และขอบเขตของ y ภายในก็จบลงเช่นกัน println! ตัวสุดท้ายจึงพิมพ์ at the end: x = Some(5), y = 10

การสร้างพจน์ match ที่เปรียบเทียบค่าของ x และ y ตัวนอก แทนที่จะเป็นการแนะนำตัวแปรใหม่ที่บดบังตัวแปร y ที่มีอยู่เดิม เราจะต้องใช้เงื่อนไข match guard แทน เราจะพูดถึง match guards ในภายหลังในส่วน “Adding Conditionals with Match Guards”

การจับคู่กับหลายรูปแบบ (Matching Multiple Patterns)

ในพจน์ match คุณสามารถจับคู่กับหลายรูปแบบได้โดยใช้ไวยากรณ์ | ซึ่งเป็นตัวดำเนินการ or ของรูปแบบ ตัวอย่างเช่น ในโค้ดต่อไปนี้ เราจับคู่ค่าของ x กับแขนของ match ซึ่งแขนแรกมีตัวเลือก or หมายความว่าหากค่าของ x จับคู่ตรงกับค่าใดค่าหนึ่งในแขนนั้น โค้ดของแขนนั้นจะทำงาน:

fn main() {
    let x = 1;

    match x {
        1 | 2 => println!("one or two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

โค้ดนี้จะพิมพ์ one or two

การจับคู่กับช่วงของค่าด้วย ..=

ไวยากรณ์ ..= อนุญาตให้เราจับคู่กับช่วงของค่าแบบนับรวม (inclusive range) ในโค้ดต่อไปนี้ เมื่อรูปแบบจับคู่ตรงกับค่าใดๆ ภายในช่วงที่กำหนด แขนนั้นจะทำงาน:

fn main() {
    let x = 5;

    match x {
        1..=5 => println!("one through five"),
        _ => println!("something else"),
    }
}

หาก x เป็น 1, 2, 3, 4, หรือ 5 แขนแรกจะจับคู่ตรงกัน ไวยากรณ์นี้สะดวกกว่าสำหรับการจับคู่หลายค่าเมื่อเทียบกับการใช้ตัวดำเนินการ | เพื่อแสดงแนวคิดเดียวกัน หากเราใช้ | เราจะต้องระบุ 1 | 2 | 3 | 4 | 5 การระบุช่วงนั้นสั้นกว่ามาก โดยเฉพาะอย่างยิ่งหากเราต้องการจับคู่ตัวเลขใดๆ ระหว่าง 1 ถึง 1,000!

คอมไพเลอร์จะตรวจสอบว่าช่วงนั้นไม่ได้ว่างเปล่าในขณะคอมไพล์ และเนื่องจากชนิดข้อมูลเดียวที่ Rust สามารถบอกได้ว่าช่วงว่างเปล่าหรือไม่คือ char และค่าตัวเลข ช่วงจึงอนุญาตให้ใช้เฉพาะกับค่าตัวเลขหรือ char เท่านั้น

นี่คือตัวอย่างการใช้ช่วงของค่า char:

fn main() {
    let x = 'c';

    match x {
        'a'..='j' => println!("early ASCII letter"),
        'k'..='z' => println!("late ASCII letter"),
        _ => println!("something else"),
    }
}

Rust สามารถบอกได้ว่า 'c' อยู่ภายในช่วงของรูปแบบแรก จึงพิมพ์ early ASCII letter

การแยกโครงสร้างเพื่อถอดส่วนประกอบของค่า

เราสามารถใช้รูปแบบเพื่อแยกโครงสร้าง (destructure) structs, enums, และ tuples เพื่อใช้ส่วนประกอบต่างๆ ของค่าเหล่านี้ได้ เรามาไล่ดูทีละชนิดข้อมูลกัน

Structs

โค้ดตัวอย่างที่ 19-12 แสดง struct Point ที่มีสองฟิลด์คือ x และ y ซึ่งเราสามารถแยกชิ้นส่วนได้โดยใช้รูปแบบร่วมกับคำสั่ง let

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 ที่จับคู่กับค่าของฟิลด์ x และ y ของ struct p ตัวอย่างนี้แสดงว่าชื่อของตัวแปรในรูปแบบไม่จำเป็นต้องตรงกับชื่อฟิลด์ของ struct อย่างไรก็ตาม เป็นเรื่องปกติที่จะกำหนดชื่อตัวแปรให้ตรงกับชื่อฟิลด์เพื่อให้ง่ายต่อการจดจำว่าตัวแปรใดมาจากฟิลด์ใด เนื่องจากรูปแบบการใช้งานทั่วไปนี้ และเนื่องจากการเขียน let Point { x: x, y: y } = p; มีความซ้ำซาก Rust จึงมีทางลัดสำหรับรูปแบบที่จับคู่กับฟิลด์ของ struct: คุณเพียงระบุชื่อของฟิลด์ใน struct และตัวแปรที่สร้างขึ้นจากรูปแบบจะมีชื่อเดียวกัน โค้ดตัวอย่างที่ 19-13 มีพฤติกรรมในลักษณะเดียวกับโค้ดตัวอย่างที่ 19-12 แต่ตัวแปรที่สร้างขึ้นในรูปแบบ let คือ x และ y แทนที่จะเป็น a และ b

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 ที่จับคู่กับฟิลด์ x และ y ของตัวแปร p ผลลัพธ์คือตัวแปร x และ y จะบรรจุค่าจาก struct p

เรายังสามารถแยกโครงสร้างโดยใช้ค่าตรงตัว (literals) เป็นส่วนหนึ่งของรูปแบบ struct แทนที่จะสร้างตัวแปรสำหรับฟิลด์ทั้งหมด การทำเช่นนั้นช่วยให้เราทดสอบฟิลด์บางฟิลด์สำหรับค่าเฉพาะเจาะจง ในขณะที่สร้างตัวแปรเพื่อแยกโครงสร้างฟิลด์อื่นๆ

ในโค้ดตัวอย่างที่ 19-14 เรามีพจน์ match ที่แยกค่า Point ออกเป็นสามกรณี: จุดที่อยู่บนแกน x โดยตรง (ซึ่งเป็นจริงเมื่อ y = 0), บนแกน y (x = 0), หรือไม่อยู่บนแกนใดเลย

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {x}"),
        Point { x: 0, y } => println!("On the y axis at {y}"),
        Point { x, y } => {
            println!("On neither axis: ({x}, {y})");
        }
    }
}

แขนแรกจะจับคู่กับจุดใดๆ ที่อยู่บนแกน x โดยระบุว่าฟิลด์ y จับคู่หากค่าของมันตรงกับค่าตรงตัว 0 รูปแบบยังคงสร้างตัวแปร x ที่เราสามารถใช้ในโค้ดสำหรับแขนนี้ได้

ในทำนองเดียวกัน แขนที่สองจะจับคู่กับจุดใดๆ บนแกน y โดยระบุว่าฟิลด์ x จับคู่หากค่าของมันเป็น 0 และสร้างตัวแปร y สำหรับค่าของฟิลด์ y แขนที่สามไม่ได้ระบุค่าตรงตัวใดๆ จึงจับคู่กับ Point อื่นๆ ทั้งหมด และสร้างตัวแปรสำหรับทั้งฟิลด์ x และ y

ในตัวอย่างนี้ ค่า p จับคู่กับแขนที่สองเนื่องจาก x บรรจุค่า 0 ดังนั้นโค้ดนี้จะพิมพ์ On the y axis at 7

โปรดจำไว้ว่าพจน์ match จะหยุดตรวจสอบแขนทันทีที่พบรูปแบบแรกที่จับคู่ตรงกัน ดังนั้นแม้ว่า Point { x: 0, y: 0 } จะอยู่ทั้งบนแกน x และแกน y โค้ดนี้จะพิมพ์เฉพาะ On the x axis at 0 เท่านั้น

Enums

เราได้แยกโครงสร้าง enums ในหนังสือเล่มนี้มาแล้ว (เช่น โค้ดตัวอย่างที่ 6-5 ในบทที่ 6) แต่เรายังไม่ได้อภิปรายอย่างชัดแจ้งว่ารูปแบบในการแยกโครงสร้าง enum นั้นสอดคล้องกับวิธีที่ข้อมูลภายใน enum ถูกนิยามไว้ ตัวอย่างเช่น ในโค้ดตัวอย่างที่ 19-15 เราใช้ enum Message จากโค้ดตัวอย่างที่ 6-2 และเขียน match ด้วยรูปแบบที่จะแยกโครงสร้างค่าภายในแต่ละค่า

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!("The Quit variant has no data to destructure.");
        }
        Message::Move { x, y } => {
            println!("Move in the x direction {x} and in the y direction {y}");
        }
        Message::Write(text) => {
            println!("Text message: {text}");
        }
        Message::ChangeColor(r, g, b) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
    }
}

โค้ดนี้จะพิมพ์ Change color to red 0, green 160, and blue 255 ลองเปลี่ยนค่าของ msg เพื่อดูโค้ดจากแขนอื่นๆ ทำงาน

สำหรับแวเรียนต์ของ enum ที่ไม่มีข้อมูล เช่น Message::Quit เราไม่สามารถแยกโครงสร้างค่านั้นเพิ่มเติมได้อีก เราสามารถจับคู่ได้เฉพาะกับค่าตรงตัว Message::Quit เท่านั้น และไม่มีตัวแปรใดๆ ในรูปแบบนั้น

สำหรับแวเรียนต์ของ enum ที่คล้าย struct เช่น Message::Move เราสามารถใช้รูปแบบที่คล้ายกับรูปแบบที่เราระบุเพื่อจับคู่ struct ได้ หลังชื่อแวเรียนต์ เราใส่วงเล็บปีกกาแล้วระบุฟิลด์พร้อมตัวแปร เพื่อให้เราแยกส่วนประกอบออกเป็นชิ้นๆ สำหรับใช้ในโค้ดของแขนนั้น ในที่นี้เราใช้รูปแบบทางลัดตามที่ทำในโค้ดตัวอย่างที่ 19-13

สำหรับแวเรียนต์ของ enum ที่คล้าย tuple เช่น Message::Write ที่เก็บ tuple มีหนึ่งองค์ประกอบ และ Message::ChangeColor ที่เก็บ tuple มีสามองค์ประกอบ รูปแบบจะคล้ายกับรูปแบบที่เราระบุเพื่อจับคู่ tuple จำนวนตัวแปรในรูปแบบต้องตรงกับจำนวนองค์ประกอบในแวเรียนต์ที่เรากำลังจับคู่

Structs และ Enums ซ้อนกัน

จนถึงตอนนี้ ตัวอย่างทั้งหมดของเราเป็นการจับคู่ structs หรือ enums ความลึกระดับเดียว แต่การจับคู่สามารถทำงานกับรายการที่ซ้อนกันได้ด้วย! ตัวอย่างเช่น เราสามารถปรับปรุงโครงสร้าง (refactor) โค้ดในโค้ดตัวอย่างที่ 19-15 เพื่อรองรับสี RGB และ HSV ในข้อความ ChangeColor ดังแสดงในโค้ดตัวอย่างที่ 19-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!("Change color to red {r}, green {g}, and blue {b}");
        }
        Message::ChangeColor(Color::Hsv(h, s, v)) => {
            println!("Change color to hue {h}, saturation {s}, value {v}");
        }
        _ => (),
    }
}

รูปแบบของแขนแรกในพจน์ match จับคู่กับแวเรียนต์ enum Message::ChangeColor ที่บรรจุแวเรียนต์ Color::Rgb จากนั้นรูปแบบจะผูกเข้ากับค่า i32 ภายในทั้งสามค่า รูปแบบของแขนที่สองก็นำไปจับคู่กับแวเรียนต์ enum Message::ChangeColor เช่นกัน แต่อนุภาค inner enum จะจับคู่กับ Color::Hsv แทน เราสามารถระบุเงื่อนไขที่ซับซ้อนเหล่านี้ในพจน์ match เดียว แม้ว่าจะมี enums สองตัวเข้ามาเกี่ยวข้องก็ตาม

Structs และ Tuples

เราสามารถผสมผสาน จับคู่ และซ้อนรูปแบบการแยกโครงสร้างในวิธีที่ซับซ้อนยิ่งขึ้นไปอีก ตัวอย่างต่อไปนี้แสดงการแยกโครงสร้างที่ซับซ้อนซึ่งเราซ้อน structs และ tuples ไว้ภายใน tuple และแยกโครงสร้างค่าดั้งเดิม (primitive values) ทั้งหมดออกมา:

fn main() {
    struct Point {
        x: i32,
        y: i32,
    }

    let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}

โค้ดนี้ช่วยให้เราแยกชนิดข้อมูลที่ซับซ้อนออกเป็นส่วนประกอบย่อยๆ เพื่อให้เราสามารถใช้ค่าที่เราสนใจแยกจากกันได้

การแยกโครงสร้างด้วยรูปแบบเป็นวิธีที่สะดวกในการใช้ส่วนต่างๆ ของค่า เช่น ค่าจากแต่ละฟิลด์ใน struct โดยแยกออกจากกัน

การละเว้นค่าในรูปแบบ

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

ละเว้นทั้งค่าด้วย _

เราได้ใช้เครื่องหมายขีดล่างเป็นรูปแบบไวลด์การ์ดที่จะจับคู่กับค่าใดๆ แต่ไม่ผูกเข้ากับค่านั้น สิ่งนี้มีประโยชน์อย่างยิ่งในฐานะแขนสุดท้ายในพจน์ match แต่เรายังสามารถใช้มันในรูปแบบใดก็ได้ รวมถึงพารามิเตอร์ของฟังก์ชัน ดังแสดงในโค้ดตัวอย่างที่ 19-17

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {y}");
}

fn main() {
    foo(3, 4);
}

โค้ดนี้จะละเว้นค่า 3 ที่ส่งมาเป็นอาร์กิวเมนต์แรกอย่างสมบูรณ์ และจะพิมพ์ This code only uses the y parameter: 4

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

ละเว้นบางส่วนของค่าด้วย _ ที่ซ้อนอยู่

เรายังสามารถใช้ _ ภายในอีกรูปแบบหนึ่งเพื่อละเว้นเพียงบางส่วนของค่าได้ ตัวอย่างเช่น เมื่อเราต้องการทดสอบเพียงบางส่วนของค่า แต่ไม่มีประโยชน์สำหรับส่วนอื่นในโค้ดที่สอดคล้องกันที่เราต้องการรัน โค้ดตัวอย่างที่ 19-18 แสดงโค้ดที่รับผิดชอบในการจัดการค่าของตั้งค่า ข้อกำหนดทางธุรกิจคือผู้ใช้ไม่ควรได้รับอนุญาตให้ทับการกำหนดค่าเดิมที่มีอยู่ของตั้งค่า แต่สามารถยกเลิกการตั้งค่าและมอบค่าให้มันได้หากปัจจุบันยังไม่ได้ถูกตั้งค่า

fn main() {
    let mut setting_value = Some(5);
    let new_setting_value = Some(10);

    match (setting_value, new_setting_value) {
        (Some(_), Some(_)) => {
            println!("Can't overwrite an existing customized value");
        }
        _ => {
            setting_value = new_setting_value;
        }
    }

    println!("setting is {setting_value:?}");
}

โค้ดนี้จะพิมพ์ Can't overwrite an existing customized value แล้วตามด้วย setting is Some(5) ในแขน match แรก เราไม่จำเป็นต้องจับคู่หรือใช้ค่าภายในแวเรียนต์ Some ตัวใดตัวหนึ่ง แต่เราจำเป็นต้องทดสอบสำหรับกรณีที่ setting_value และ new_setting_value เป็นแวเรียนต์ Some ในกรณีนั้น เราจะพิมพ์เหตุผลในการไม่เปลี่ยน setting_value และมันจะไม่ถูกเปลี่ยน

ในกรณีอื่นๆ ทั้งหมด (หาก setting_value หรือ new_setting_value เป็น None) ซึ่งถูกแสดงโดยรูปแบบ _ ในแขนที่สอง เราต้องการอนุญาตให้ new_setting_value กลายเป็น setting_value

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

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {first}, {third}, {fifth}");
        }
    }
}

โค้ดนี้จะพิมพ์ Some numbers: 2, 8, 32 และค่า 4 กับ 16 จะถูกละเว้น

ตัวแปรที่ไม่ถูกใช้งานโดยการเริ่มต้นชื่อด้วย _

หากคุณสร้างตัวแปรแต่ไม่ได้ใช้มันที่ไหนเลย โดยปกติ Rust จะออกคำเตือนเพราะตัวแปรที่ไม่ถูกใช้งานอาจเป็นบั๊กได้ อย่างไรก็ตาม ในบางครั้งมีประโยชน์ที่จะสามารถสร้างตัวแปรที่คุณจะยังไม่ใช้ เช่น เมื่อคุณกำลังทำต้นแบบ (prototyping) หรือเพิ่งเริ่มโปรเจกต์ ในสถานการณ์นี้ คุณสามารถบอก Rust ไม่ให้เตือนคุณเกี่ยวกับตัวแปรที่ไม่ถูกใช้งานโดยการเริ่มต้นชื่อของตัวแปรด้วยขีดล่าง ในโค้ดตัวอย่างที่ 19-20 เราสร้างตัวแปรที่ไม่ถูกใช้งานสองตัว แต่เมื่อเราคอมไพล์โค้ดนี้ เราควรร่วมได้รับคำเตือนเฉพาะเกี่ยวกับตัวแปรตัวเดียวเท่านั้น

fn main() {
    let _x = 5;
    let y = 10;
}

ในที่นี้ เราได้รับคำเตือนเกี่ยวกับการไม่ใช้ตัวแปร y แต่เราไม่ได้รับคำเตือนเกี่ยวกับการไม่ใช้ _x

โปรดสังเกตว่ามีความแตกต่างอย่างแยบยลระหว่างการใช้เพียง _ กับการใช้ชื่อที่เริ่มต้นด้วยขีดล่าง ไวยากรณ์ _x ยังคงผูกค่าเข้ากับตัวแปร ในขณะที่ _ ไม่ผูกเลย ในการแสดงกรณีที่ความแตกต่างนี้มีความสำคัญ โค้ดตัวอย่างที่ 19-21 จะให้ข้อผิดพลาดแก่เรา

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_s) = s {
        println!("found a string");
    }

    println!("{s:?}");
}

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

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_) = s {
        println!("found a string");
    }

    println!("{s:?}");
}

โค้ดนี้ทำงานได้เป็นอย่างดีเพราะเราไม่มีวันผูก s เข้ากับอะไรเลย มันจึงไม่ถูกย้าย

ส่วนที่เหลือของค่าด้วย ..

กับค่าที่มีหลายส่วน เราสามารถใช้ไวยากรณ์ .. เพื่อใช้เฉพาะส่วนที่สนใจและละเว้นส่วนที่เหลือ ช่วยหลีกเลี่ยงความจำเป็นในการระบุขีดล่างสำหรับแต่ละค่าที่ถูกละเว้น รูปแบบ .. จะละเว้นส่วนใดๆ ของค่าที่เราไม่ได้จับคู่อย่างชัดแจ้งในส่วนที่เหลือของรูปแบบ ในโค้ดตัวอย่างที่ 19-23 เรามี struct 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 is {x}"),
    }
}

เราระบุค่า x แล้วเพียงแค่ใส่รูปแบบ .. เข้าไป สิ่งนี้เร็วกว่าการต้องระบุ y: _ และ z: _ โดยเฉพาะเมื่อเราทำงานกับ structs ที่มีฟิลด์จำนวนมากในสถานการณ์ที่มีเพียงหนึ่งหรือสองฟิลด์เท่านั้นที่เกี่ยวข้อง

ไวยากรณ์ .. จะขยายครอบคลุมไปยังค่าจำนวนเท่าใดก็ได้ตามที่ต้องการ โค้ดตัวอย่างที่ 19-24 แสดงวิธีใช้ .. กับ tuple

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {first}, {last}");
        }
    }
}

ในโค้ดนี้ ค่าแรกและค่าสุดท้ายจะถูกจับคู่กับ first และ last ตัว .. จะจับคู่และละเว้นทุกอย่างในตรงกลาง

อย่างไรก็ตาม การใช้ .. ต้องไม่มีความกำกวม (unambiguous) หากไม่ชัดเจนว่าค่าใดตั้งใจไว้สำหรับการจับคู่และค่าใดควรละเว้น Rust จะแจ้งข้อผิดพลาดแก่เรา โค้ดตัวอย่างที่ 19-25 แสดงตัวอย่างการใช้ .. ในวิธีที่กำกวม ดังนั้นจึงไม่สามารถคอมไพล์ได้

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {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` (bin "patterns") due to 1 previous error

เป็นไปไม่ได้ที่ Rust จะกำหนดว่าต้องละเว้นกี่ค่าใน tuple ก่อนที่จะจับคู่ค่ากับ second และจากนั้นต้องละเว้นค่าอีกกี่ค่าต่อไป โค้ดนี้อาจหมายความว่าเราต้องการละเว้น 2, ผูก second เข้ากับ 4, และจากนั้นละเว้น 8, 16, และ 32; หรือเราต้องการละเว้น 2 และ 4, ผูก second เข้ากับ 8, และจากนั้นละเว้น 16 และ 32; และอื่น ๆ ชื่อตัวแปร second ไม่ได้มีความหมายพิเศษใดๆ สำหรับ Rust ดังนั้นเราจึงได้รับข้อผิดพลาดจากคอมไพเลอร์ เนื่องจากการใช้ .. ในสองตำแหน่งเช่นนี้มีความกำกวม

การเพิ่มเงื่อนไขด้วย Match Guards

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

เงื่อนไขสามารถใช้ตัวแปรที่สร้างขึ้นในรูปแบบได้ โค้ดตัวอย่างที่ 19-26 แสดง match ที่แขนแรกมีรูปแบบ Some(x) และยังมี match guard เป็น if x % 2 == 0 (ซึ่งจะเป็น true หากตัวเลขเป็นเลขคู่)

fn main() {
    let num = Some(4);

    match num {
        Some(x) if x % 2 == 0 => println!("The number {x} is even"),
        Some(x) => println!("The number {x} is odd"),
        None => (),
    }
}

ตัวอย่างนี้จะพิมพ์ The number 4 is even เมื่อ num นำมาเปรียบเทียบกับรูปแบบในแขนแรก มันจะจับคู่ตรงเพราะ Some(4) ตรงกับ Some(x) จากนั้น match guard จะตรวจสอบว่าเศษจากการหาร x ด้วย 2 เท่ากับ 0 หรือไม่ และเนื่องจากมันเท่ากับ 0 แขนแรกจึงถูกเลือก

หาก num เป็น Some(5) แทน match guard ในแขนแรกจะเป็น false เพราะเศษจากการหาร 5 ด้วย 2 คือ 1 ซึ่งไม่เท่ากับ 0 จากนั้น Rust จะไปยังแขนที่สอง ซึ่งจะจับคู่ตรงเพราะแขนที่สองไม่มี match guard จึงจับคู่กับแวเรียนต์ Some ใดๆ

ไม่มีวิธีแสดงเงื่อนไข if x % 2 == 0 ภายในรูปแบบได้ ดังนั้น match guard จึงมอบความสามารถในการแสดงตรรกะนี้แก่เรา ข้อเสียของการแสดงออกที่เพิ่มขึ้นนี้คือ คอมไพเลอร์จะไม่พยายามตรวจสอบความครอบคลุมทุกกรณี (exhaustiveness) เมื่อมีพจน์ match guard เข้ามาเกี่ยวข้อง

เมื่ออภิปรายโค้ดตัวอย่างที่ 19-11 เรากล่าวว่าเราสามารถใช้ match guards เพื่อแก้ปัญหา pattern-shadowing ของเราได้ ลองนึกดูว่าเราสร้างตัวแปรใหม่ภายในรูปแบบในพจน์ match แทนที่จะใช้ตัวแปนอก match ตัวแปรใหม่นั้นทำให้เราไม่สามารถทดสอบเปรียบเทียบกับค่าของตัวแปรตัวนอกได้ โค้ดตัวอย่างที่ 19-27 แสดงวิธีที่เราสามารถใช้ match guard เพื่อแก้ไขปัญหานี้

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}

ตอนนี้โค้ดนี้จะพิมพ์ Default case, x = Some(5) รูปแบบในแขน match ที่สองไม่ได้แนะนำตัวแปร y ใหม่ที่จะมาบดบัง y ตัวนอก หมายความว่าเราสามารถใช้ y ตัวนอกใน match guard ได้ แทนที่จะระบุรูปแบบเป็น Some(y) ซึ่งจะบดบัง y ตัวนอก เราจะระบุ Some(n) แทน สิ่งนี้สร้างตัวแปรใหม่ n ที่ไม่ได้บดบังอะไรเลย เพราะไม่มีตัวแปร n นอก match

match guard if n == y ไม่ใช่รูปแบบ ดังนั้นจึงไม่ได้แนะนำตัวแปรใหม่ ตัว y นี้ คือ y ตัวนอก แทนที่จะเป็น y ใหม่ที่มาบดบังมัน และเราสามารถมองหาค่าที่มีค่าเดียวกับ y ตัวนอกได้โดยการเปรียบเทียบ n กับ y

คุณยังสามารถใช้ตัวดำเนินการ or | ใน match guard เพื่อระบุหลายรูปแบบ; เงื่อนไข match guard จะส่งผลประยุกต์ใช้กับรูปแบบทั้งหมด โค้ดตัวอย่างที่ 19-28 แสดงลำดับความสำคัญเมื่อรวมรูปแบบที่ใช้ | เข้ากับ match guard ส่วนสำคัญของตัวอย่างนี้คือ match guard if y ส่งผลประยุกต์ใช้กับ 4, 5, และ 6 แม้ว่าจะอาจดูเหมือน if y ส่งผลเฉพาะกับ 6 ก็ตาม

fn main() {
    let x = 4;
    let y = false;

    match x {
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"),
    }
}

เงื่อนไข match ระบุว่าแขนจะจับคู่ตรงก็ต่อเมื่อค่าของ x เท่ากับ 4, 5, หรือ 6 และ หาก y เป็น true เมื่อโค้ดนี้ทำงาน รูปแบบของแขนแรกจับคู่ตรงเพราะ x คือ 4 แต่ match guard if y เป็น false แขนแรกจึงไม่ถูกเลือก โค้ดจะย้ายไปยังแขนที่สอง ซึ่งจับคู่ตรง และโปรแกรมนี้จะพิมพ์ no เหตุผลคือเงื่อนไข if ส่งผลต่อรูปแบบทั้งหมด 4 | 5 | 6 ไม่ใช่แค่ค่าสุดท้าย 6 กล่าวอีกนัยหนึ่ง ลำดับความสำคัญของ match guard ในความสัมพันธ์กับรูปแบบมีพฤติกรรมเช่นนี้:

(4 | 5 | 6) if y => ...

แทนที่จะเป็นเช่นนี้:

4 | 5 | (6 if y) => ...

หลังจากรันโค้ด พฤติกรรมลำดับความสำคัญก็เห็นได้ชัด: หาก match guard ถูกประยุกต์ใช้เฉพาะกับค่าสุดท้ายในรายการค่าที่ระบุโดยใช้ตัวดำเนินการ | แขนนั้นก็จะจับคู่ตรง และโปรแกรมก็จะพิมพ์ yes

การใช้ @ Bindings

ตัวดำเนินการ at @ ช่วยให้เราสามารถสร้างตัวแปรที่เก็บค่าไปพร้อมๆ กับที่เรากำลังทดสอบค่านั้นสำหรับการจับคู่รูปแบบ ในโค้ดตัวอย่างที่ 19-29 เราต้องการทดสอบว่าฟิลด์ id ของ Message::Hello อยู่ภายในช่วง 3..=7 หรือไม่ เรายังต้องการผูกค่านั้นเข้ากับตัวแปร id เพื่อให้เราสามารถใช้มันในโค้ดที่เกี่ยวข้องกับแขนนั้นได้

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello { id: id @ 3..=7 } => {
            println!("Found an id in range: {id}")
        }
        Message::Hello { id: 10..=12 } => {
            println!("Found an id in another range")
        }
        Message::Hello { id } => println!("Found some other id: {id}"),
    }
}

ตัวอย่างนี้จะพิมพ์ Found an id in range: 5 โดยการระบุ id @ ก่อนช่วง 3..=7 เรากำลังจับค่าใดก็ตามที่จับคู่ตรงกับช่วงไว้ในตัวแปรชื่อ id ไปพร้อมกับทดสอบด้วยว่าค่านั้นจับคู่ตรงกับรูปแบบช่วงด้วย

ในแขนที่สอง ซึ่งเรามีเฉพาะช่วงที่ระบุในรูปแบบ โค้ดที่เกี่ยวข้องกับแขนไม่ได้มีตัวแปรที่บรรจุค่าจริงของฟิลด์ id ค่าของฟิลด์ id อาจเป็น 10, 11, หรือ 12 แต่โค้ดที่ไปกับรูปแบบนั้นไม่รู้ว่าเป็นตัวไหน โค้ดรูปแบบไม่สามารถใช้ค่าจากฟิลด์ id ได้เพราะเราไม่ได้บันทึกค่า id ไว้ในตัวแปร

ในแขนสุดท้าย ซึ่งเราได้ระบุตัวแปรโดยไม่มีช่วง เรามีค่าพร้อมใช้ในโค้ดของแขนในตัวแปรชื่อ id เหตุผลคือเราได้ใช้ไวยากรณ์ทางลัดฟิลด์ struct แต่เราไม่ได้ประยุกต์ใช้การทดสอบใดๆ กับค่าในฟิลด์ id ในแขนนี้ ดังที่เราทำกับสองแขนแรก: ค่าใดๆ ก็ตามจะจับคู่ตรงกับรูปแบบนี้

การใช้ @ ช่วยให้เราทดสอบค่าและบันทึกไว้ในตัวแปรภายในรูปแบบเดียวได้

สรุป

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

ถัดไป สำหรับบทรองสุดท้ายของหนังสือเล่มนี้ เราจะดูแง่มุมระดับสูงของคุณลักษณะต่างๆ ของ Rust