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

การเขียนโปรแกรมเกมทายตัวเลข

มาเริ่มก้าวเข้าสู่ภาษา Rust ด้วยการลงมือทำโปรเจกต์จริงไปด้วยกัน! บทนี้จะแนะนำคุณให้รู้จักกับแนวคิดพื้นฐานทั่วไปของ Rust ผ่านการเขียนโปรแกรมใช้งานจริง คุณจะได้เรียนรู้เกี่ยวกับ let, match, เมธอด (methods), ฟังก์ชันที่เกี่ยวข้อง (associated functions), เครตภายนอก (external crates) และอีกมากมาย! ในบทต่อๆ ไป เราจะได้สำรวจแนวคิดเหล่านั้นอย่างละเอียด สำหรับบทนี้ คุณจะได้ฝึกฝนเฉพาะส่วนที่เป็นพื้นฐานที่จำเป็นเท่านั้น

เราจะมาพัฒนาโปรแกรมที่เป็นโจทย์สุดคลาสสิกสำหรับผู้เริ่มต้นเรียนรู้การเขียนโปรแกรม นั่นคือ เกมทายตัวเลข รูปแบบการทำงานคือ โปรแกรมจะสุ่มตัวเลขจำนวนเต็มระหว่าง 1 ถึง 100 จากนั้นจะแจ้งเตือนให้ผู้เล่นป้อนตัวเลขที่ทาย เมื่อผู้เล่นป้อนคำตอบแล้ว โปรแกรมจะบอกว่าตัวเลขที่ทายนั้นต่ำเกินไปหรือสูงเกินไป หากทายถูกต้อง เกมจะแสดงข้อความแสดงความยินดีและจบการทำงาน

การตั้งค่าโปรเจกต์ใหม่

สำหรับการตั้งค่าโปรเจกต์ใหม่ ให้เข้าไปยังไดเรกทอรี projects ที่คุณสร้างขึ้นในบทที่ 1 แล้วสร้างโปรเจกต์ใหม่โดยใช้ Cargo ดังนี้:

$ cargo new guessing_game
$ cd guessing_game

คำสั่งแรก cargo new จะใช้ชื่อโปรเจกต์ (guessing_game) เป็นอาร์กิวเมนต์แรก คำสั่งที่สองจะสลับไปยังไดเรกทอรีของโปรเจกต์ใหม่

ลองมาดูไฟล์ Cargo.toml ที่ถูกสร้างขึ้นกัน:

Filename: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"

[dependencies]

ตามที่คุณได้เห็นในบทที่ 1 คำสั่ง cargo new จะสร้างโปรแกรม “Hello, world!” ให้คุณโดยอัตโนมัติ ลองมาตรวจสอบไฟล์ src/main.rs กัน:

Filename: src/main.rs

fn main() {
    println!("Hello, world!");
}

คราวนี้มาลองคอมไพล์โปรแกรม “Hello, world!” นี้และรันไปในขั้นตอนเดียวกันด้วยคำสั่ง cargo run:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/guessing_game`
Hello, world!

คำสั่ง run จะมีประโยชน์อย่างยิ่งเมื่อคุณต้องการปรับแก้และพัฒนาโปรเจกต์อย่างรวดเร็ว (rapid iterate) ดังที่เราจะทำกันในเกมนี้ เพื่อตรวจสอบผลในแต่ละรอบการแก้ไขอย่างรวดเร็วก่อนที่จะดำเนินงานในขั้นตอนถัดไป

เปิดไฟล์ src/main.rs ขึ้นมาอีกครั้ง คุณจะเขียนโค้ดทั้งหมดลงในไฟล์นี้

การประมวลผลค่าที่ทาย

ส่วนแรกของโปรแกรมเกมทายตัวเลขคือการร้องขออินพุตจากผู้ใช้งาน ประมวลผลอินพุตดังกล่าว และตรวจสอบว่าอินพุตอยู่ในรูปแบบที่คาดหวัง ในตอนเริ่มต้น เราจะปล่อยให้ผู้เล่นป้อนตัวเลขที่ต้องการทาย ให้เขียนโค้ดในรายการที่ 2-1 ลงในไฟล์ src/main.rs

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

โค้ดนี้มีข้อมูลต่างๆ อยู่มากมาย มาลองทำความเข้าใจกันไปทีละบรรทัด เพื่อที่จะรับค่าอินพุตจากผู้ใช้และแสดงผลลัพธ์ที่ได้ออกมาทางเอาต์พุต เราจำเป็นต้องนำเข้าไลบรารีสำหรับนำข้อมูลเข้า/ออก (input/output) ชื่อว่า io เข้ามาในขอบเขต (scope) ไลบรารี io นี้มาจากไลบรารีมาตรฐานที่ชื่อว่า std:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

โดยค่าเริ่มต้น Rust จะมีชุดไอเทมที่กำหนดไว้ในไลบรารีมาตรฐานซึ่งจะถูกนำเข้าสู่ขอบเขตการทำงานของทุกๆ โปรแกรมโดยอัตโนมัติ ชุดไอเทมนี้ถูกเรียกว่า prelude คุณสามารถดูรายละเอียดทั้งหมดที่มีอยู่ในนั้นได้ที่ เอกสารประกอบการใช้งานของไลบรารีมาตรฐาน

หากประเภทข้อมูล (type) ที่คุณต้องการใช้งานไม่ได้อยู่ใน prelude คุณต้องนำเข้าขอบเขตนั้นอย่างเจาะจงผ่านคำสั่ง use การใช้งานไลบรารี std::io จะช่วยอำนวยความสะดวกในการใช้งานฟีเจอร์ที่มีประโยชน์หลายตัว รวมถึงการรับข้อมูลอินพุตจากผู้ใช้ด้วย

และดังที่คุณได้เห็นไปแล้วในบทที่ 1 ฟังก์ชัน main คือจุดเริ่มต้นการทำงานของโปรแกรม:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

ไวยากรณ์ fn ใช้เพื่อประกาศฟังก์ชันใหม่ วงเล็บ () ระบุว่าฟังก์ชันนี้ไม่มีพารามิเตอร์ และปีกกา { จะเริ่มต้นเนื้อหาของฟังก์ชัน

ตามที่คุณได้เรียนรู้ในบทที่ 1 มาโคร println! จะมีหน้าที่พิมพ์ข้อความลงบนหน้าจอ:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

โค้ดส่วนนี้จะพิมพ์คำแนะนำเพื่อบอกว่านี่คือเกมอะไรและขอรับข้อมูลจากผู้ใช้

การเก็บค่าด้วยตัวแปร

ถัดไป เราจะสร้าง ตัวแปร (variable) เพื่อเก็บข้อมูลอินพุตของผู้ใช้ ดังนี้:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

ตอนนี้โปรแกรมเริ่มน่าสนใจขึ้นแล้ว! มีการทำงานเกิดขึ้นหลายอย่างในบรรทัดเล็กๆ นี้ เราใช้งานคำสั่ง let เพื่อสร้างตัวแปรขึ้นมา ตัวอย่างเช่น:

let apples = 5;

บรรทัดนี้จะสร้างตัวแปรใหม่ชื่อ apples และผูก (bind) ค่าเข้ากับเลข 5 ในภาษา Rust ตัวแปรจะไม่สามารถเปลี่ยนแปลงค่าได้เป็นค่าเริ่มต้น (immutable by default) ซึ่งหมายความว่าเมื่อเราให้ค่าแก่ตัวแปรนั้นแล้ว ค่านั้นจะไม่สามารถเปลี่ยนแปลงได้อีก เราจะหารือเกี่ยวกับแนวคิดนี้โดยละเอียดในหัวข้อ “ตัวแปรและการแก้ไขค่าได้” ในบทที่ 3 หากต้องการทำให้ตัวแปรสามารถแก้ไขค่าได้ เราจะต้องเติม mut หน้าชื่อตัวแปร:

let apples = 5; // immutable
let mut bananas = 5; // mutable

หมายเหตุ: รูปแบบ // เป็นการเริ่มต้นการเขียนคำอธิบายโค้ด (comments) ซึ่งจะมีผลไปจนถึงสิ้นสุดบรรทัด Rust จะเพิกเฉยต่อข้อมูลทุกอย่างที่อยู่ในคำอธิบายโค้ด เราจะพูดคุยเกี่ยวกับคำอธิบายโค้ดเพิ่มเติมใน บทที่ 3

ย้อนกลับมาที่โปรแกรมเกมทายตัวเลขของเรา ตอนนี้คุณทราบแล้วว่า let mut guess จะเป็นการสร้างตัวแปรที่แก้ไขค่าได้ชื่อว่า guess เครื่องหมายเท่ากับ (=) จะคอยบอก Rust ว่าเราต้องการนำบางสิ่งมาผูกเข้ากับตัวแปรในตอนนี้ ส่วนทางขวาของเครื่องหมายเท่ากับคือค่าที่ตัวแปร guess จะถูกผูกด้วย ซึ่งได้จากผลการเรียกฟังก์ชัน String::new ซึ่งเป็นฟังก์ชันที่สร้างออบเจกต์อินสแตนซ์ใหม่ของประเภทข้อมูล String โดยที่ String เป็นประเภทของข้อความ (string type) ที่มีให้ในไลบรารีมาตรฐาน ซึ่งมีขนาดที่ขยายเพิ่มได้ และเข้ารหัสข้อความด้วย UTF-8

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

สรุปโดยรวมแล้ว บรรทัด let mut guess = String::new(); จะสร้างตัวแปรที่สามารถแก้ไขค่าได้ซึ่งในปัจจุบันถูกผูกไว้กับอินสแตนซ์ใหม่ที่ว่างเปล่าของประเภทข้อมูล String นั่นเอง 휴!

การรับข้อมูลอินพุตจากผู้ใช้

จำได้ไหมว่าเราได้นำเอาฟังก์ชันการนำเข้าและแสดงผล (input/output) จากไลบรารีมาตรฐานเข้ามาด้วยคำสั่ง use std::io; ในบรรทัดแรกของโปรแกรม คราวนี้เราจะเรียกใช้ฟังก์ชัน stdin จากโมดูล io ซึ่งจะช่วยให้เราสามารถจัดการกับอินพุตของผู้ใช้งานได้:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

หากเราไม่ได้นำเข้าโมดูล io ด้วยคำสั่ง use std::io; ตั้งแต่แรกเริ่มของโปรแกรม เราก็ยังสามารถเรียกใช้งานฟังก์ชันนี้ได้อยู่ดีโดยเขียนว่า std::io::stdin โดยฟังก์ชัน stdin จะส่งกลับอินสแตนซ์ของประเภทข้อมูล std::io::Stdin ซึ่งทำหน้าที่เป็นตัวจัดการ (handle) สำหรับการรับข้อมูลเข้ามาตรฐาน (standard input) ของเทอร์มินัลของคุณ

บรรทัดถัดมาคือ .read_line(&mut guess) จะเรียกใช้งานเมธอด read_line บนตัวจัดการการรับข้อมูลมาตรฐานเพื่อรับข้อมูลจากผู้ใช้งาน เรายังต้องส่งอาร์กิวเมนต์เป็น &mut guess ให้กับ read_line เพื่อบอกว่าจะให้บันทึกผลอินพุตของผู้ใช้ลงในข้อความใด หน้าที่ทั้งหมดของ read_line คือนำสิ่งใดก็ตามที่ผู้ใช้งานพิมพ์เข้ามาในข้อมูลเข้ามาตรฐานมาต่อท้าย (append) ลงไปในข้อความที่เรากำหนด (โดยไม่ลบเนื้อหาเก่าทิ้ง) ดังนั้นเราจึงส่งผ่านข้อความนั้นเป็นอาร์กิวเมนต์ และข้อความดังกล่าวจะต้องสามารถแก้ไขค่าได้ (mutable) เมธอดนี้จึงจะสามารถปรับเปลี่ยนเนื้อหาภายในข้อความได้

เครื่องหมาย & บ่งบอกว่าอาร์กิวเมนต์ตัวนี้คือ การอ้างอิง (reference) ซึ่งช่วยให้ส่วนต่างๆ ของโค้ดสามารถเข้าถึงข้อมูลชิ้นเดียวกันได้โดยไม่จำเป็นต้องคัดลอกข้อมูลนั้นลงบนหน่วยความจำซ้ำๆ หลายรอบ การอ้างอิง (references) ถือเป็นคุณลักษณะที่มีความซับซ้อน และหนึ่งในข้อดีหลักของ Rust ก็คือความปลอดภัยและความสะดวกในการใช้งานเรื่องดังกล่าว คุณไม่จำเป็นต้องเรียนรู้รายละเอียดทั้งหมดนี้เพื่อที่จะเขียนโปรแกรมนี้ให้เสร็จสมบูรณ์ สำหรับตอนนี้ สิ่งที่คุณต้องรู้คือ การอ้างอิงก็เหมือนกับตัวแปรทั่วไป คือจะไม่สามารถแก้ไขค่าได้เป็นค่าเริ่มต้น (immutable by default) ดังนั้นคุณจึงจำเป็นต้องเขียน &mut guess แทนที่จะเป็น &guess เพื่อทำให้แก้ไขค่าได้ (บทที่ 4 จะอธิบายเรื่องการอ้างอิงอย่างละเอียดอีกครั้ง)

การจัดการกับข้อผิดพลาดที่อาจเกิดขึ้นด้วย Result

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

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

เราสามารถเขียนโค้ดนี้ย่อลงเป็นบรรทัดเดียวได้ดังนี้:

io::stdin().read_line(&mut guess).expect("Failed to read line");

อย่างไรก็ตาม การเขียนบรรทัดยาวๆ บรรทัดเดียวจะอ่านได้ค่อนข้างยาก ดังนั้นการแบ่งออกเป็นส่วนๆ จึงดีกว่า เมื่อเรียกใช้งานเมธอดด้วยรูปแบบ .method_name() การเว้นบรรทัดใหม่รวมถึงระยะการย่อหน้าจะช่วยทำให้อ่านง่ายขึ้นมาก คราวนี้เรามาดูกันว่าบรรทัดดังกล่าวทำงานอย่างไร

ดังที่ได้กล่าวไว้ก่อนหน้านี้ว่า read_line จะนำข้อความที่ผู้ใช้พิมพ์มาเก็บไว้ในอาร์กิวเมนต์ที่เราส่งผ่านไปให้ แต่ก็ยังส่งกลับค่าประเภท Result มาด้วย ซึ่ง Result คือ ประเภทข้อมูลแจกแจงค่า (enumeration) หรือที่เรียกสั้นๆ ว่า enum ซึ่งก็คือประเภทข้อมูลที่สามารถมีสถานะที่เป็นไปได้หลายแบบ เราเรียกแต่ละสถานะที่เป็นไปได้นั้นว่า ตัวแปรรูปแบบ (variant)

ใน บทที่ 6 จะอธิบายเรื่อง enums อย่างละเอียดอีกครั้ง วัตถุประสงค์ของประเภทข้อมูล Result นี้ก็คือการเข้ารหัสข้อมูลสำหรับใช้ในการจัดการกับข้อผิดพลาด (error-handling)

ตัวแปรรูปแบบ (variants) ของ Result คือ Ok และ Err โดยรูปแบบ Ok จะเป็นตัวบ่งบอกว่าการทำงานดังกล่าวประสบความสำเร็จและมีค่าผลลัพธ์ที่สร้างได้สำเร็จบรรจุอยู่ภายใน ส่วนรูปแบบ Err จะหมายถึงการทำงานเกิดข้อผิดพลาดและจะมีข้อมูลที่บอกว่าความผิดพลาดนั้นเกิดขึ้นได้อย่างไรหรือเพราะเหตุใดบรรจุอยู่

ค่าของข้อมูลประเภท Result ก็เหมือนกับประเภทอื่นๆ คือมีเมธอดต่างๆ ที่ถูกประกาศไว้บนตัวมัน อินสแตนซ์ของ Result จะมีเมธอด expect ที่สามารถเรียกใช้งานได้ หากผลลัพธ์ของ Result นั้นมีค่าเป็น Err การเรียก expect จะทำให้โปรแกรมหยุดทำงาน (crash) และจะแสดงข้อความที่คุณส่งไปเป็นอาร์กิวเมนต์ของ expect บนหน้าจอ หากเมธอด read_line คืนค่ากลับมาเป็น Err มักเกิดจากข้อผิดพลาดที่ส่งมาจากฝั่งระบบปฏิบัติการพื้นฐาน และถ้าหากผลลัพธ์ของ Result เป็นค่า Ok ตัวเมธอด expect จะดึงเอาผลลัพธ์ค่าจริงที่อยู่ในรูปแบบ Ok นั้นออกมาและส่งคืนให้กับคุณเพื่อนำไปใช้งานต่อได้ทันที ซึ่งในกรณีนี้ ค่าดังกล่าวก็คือจำนวนขนาดไบต์ของอินพุตที่ผู้ใช้พิมพ์เข้ามา

หากคุณไม่ยอมเรียกใช้งาน expect โปรแกรมจะยังสามารถคอมไพล์ผ่านอยู่ได้ แต่คุณจะได้รับข้อความแจ้งเตือนความปลอดภัย (warning):

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
   |
10 |     let _ = io::stdin().read_line(&mut guess);
   |     +++++++

warning: `guessing_game` (bin "guessing_game") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s

Rust แจ้งเตือนว่าคุณไม่ได้ใช้ค่า Result ที่คืนกลับมาจากเมธอด read_line ซึ่งหมายความว่าตัวโปรแกรมยังไม่ได้เตรียมการรับมือกับข้อผิดพลาดที่อาจจะเกิดขึ้น

วิธีการแก้ไขคำเตือนนี้ให้ถูกต้องจริงๆ คือการเขียนโค้ดจัดการกับข้อผิดพลาด (error-handling code) แต่สำหรับกรณีของเรานี้ เราแค่ต้องการให้โปรแกรมหยุดทำงานทันทีเมื่อเกิดข้อผิดพลาดขึ้น ดังนั้นเราจึงสามารถเลือกใช้เมธอด expect ได้ คุณจะได้เรียนรู้เกี่ยวกับการกู้คืนระบบจากข้อผิดพลาดต่างๆ ใน บทที่ 9

การแสดงผลค่าตัวแปรด้วยตัวสำรองที่นั่ง (Placeholders) ของ println!

นอกจากปีกกาปิดแล้ว ก็มีเพียงแค่อีกหนึ่งบรรทัดเท่านั้นที่เราจะมาอธิบายกันจากโค้ดที่เขียนไว้ก่อนหน้านี้:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

บรรทัดนี้จะพิมพ์ข้อความซึ่งตอนนี้นั้นเก็บค่าอินพุตของผู้ใช้งานเอาไว้ เครื่องหมายปีกกา {} ทำหน้าที่เป็นตัวแทนช่องว่างสำหรับเติมคำ (placeholder): ให้นึกถึง {} เหมือนกับก้ามปูตัวเล็กๆ ที่คอยหนีบค่าเอาไว้ไม่ให้หายไป เมื่อต้องการแสดงผลลัพธ์ของตัวแปร เราสามารถระบุชื่อของตัวแปรลงไปในเครื่องหมายปีกกานั้นได้ และเมื่อต้องการพิมพ์ค่าที่เกิดจากการประมวลผลนิพจน์ (expression) ให้ใส่เครื่องหมายปีกกาเปล่าในข้อความหลัก แล้วตามด้วยรายการนิพจน์ที่คั่นด้วยเครื่องหมายจุลภาค (comma) เพื่อพิมพ์ลงในช่องว่างนั้นตามลำดับ การพิมพ์ค่าของตัวแปรและผลลัพธ์ของนิพจน์ด้วยคำสั่ง println! ในครั้งเดียวจะมีรูปแบบดังนี้:

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

println!("x = {x} and y + 2 = {}", y + 2);
}

โค้ดนี้จะแสดงผลออกมาเป็น x = {x} and y + 2 = {}, y + 2 ในกรณีทั่วไป แต่สำหรับโค้ดนี้จะพิมพ์ x = 5 and y + 2 = 12

การทดสอบการทำงานส่วนแรก

มาทดสอบการทำงานส่วนแรกของเกมทายตัวเลขกัน โดยให้ทดลองรันโดยการใช้คำสั่ง cargo run:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

มาถึงจุดนี้ การทำงานในส่วนแรกของเกมก็สำเร็จลุล่วงแล้ว: เราสามารถรับข้อมูลนำเข้าจากคีย์บอร์ดแล้วจึงแสดงข้อความนั้นกลับออกมาได้

การสร้างตัวเลขปริศนา

ขั้นตอนต่อไปคือเราต้องการสร้างตัวเลขปริศนาขึ้นมาเพื่อให้ผู้ใช้นำมาใช้ในการทาย ซึ่งตัวเลขปริศนานี้ควรจะเปลี่ยนไปในทุกๆ รอบเพื่อให้เกมน่าเล่นมากกว่าหนึ่งครั้ง เราจะสุ่มตัวเลขระหว่าง 1 ถึง 100 เพื่อให้เกมน่าเล่นและไม่ยากเกินไป ภาษา Rust ยังไม่ได้เพิ่มฟังก์ชันสำหรับการสุ่มตัวเลขเข้ามาในไลบรารีมาตรฐานของตนเอง อย่างไรก็ตาม ทีมงาน Rust ก็ได้สร้างเครื่องมือตัวช่วยเป็น rand crate ที่มาพร้อมความสามารถในการสุ่มตัวเลขนี้ไว้ให้ใช้งาน

การเพิ่มฟังก์ชันการทำงานด้วยเครต (Crate)

จำได้ไหมว่าเครต (crate) คือชุดไฟล์ซอร์สโค้ดของภาษา Rust โปรเจกต์ที่เรากำลังสร้างขึ้นนี้ถือเป็นเครตไบนารี (binary crate) ซึ่งก็คือไฟล์ที่รันได้โดยตรง ส่วนเครต rand จะเป็นเครตไลบรารี (library crate) ซึ่งเป็นโค้ดที่มีเจตนาสำหรับนำไปให้โปรแกรมอื่นๆ เรียกใช้งานร่วมกันและตัวมันเองไม่สามารถรันแยกต่างหากแบบอิสระได้

การจัดการเครตภายนอกต่างๆ ถือเป็นจุดเด่นหลักของ Cargo ก่อนที่เราจะสามารถเริ่มเขียนโค้ดเพื่อเรียกใช้งาน rand ได้ เราจำเป็นต้องทำการแก้ไขไฟล์ Cargo.toml เพื่อเพิ่มเครต rand เข้าไปในรายการไลบรารีที่ต้องใช้พึ่งพาก่อน ให้ลองเปิดไฟล์นั้นขึ้นมาตอนนี้ แล้วเพิ่มบรรทัดดังต่อไปนี้ลงไปที่ด้านล่างสุดของส่วนหัวข้อ [dependencies] ที่ Cargo สร้างไว้ให้ โปรดเขียนระบุ rand และหมายเลขเวอร์ชันให้ตรงตามตัวอย่างนี้ทุกประการ ไม่เช่นนั้นตัวอย่างในบทเรียนนี้อาจรันไม่ผ่านได้:

Filename: Cargo.toml

[dependencies]
rand = "0.10.1"

ในไฟล์ Cargo.toml ข้อมูลทั้งหมดที่อยู่ภายใต้หัวข้อส่วนหนึ่งๆ จะถือเป็นส่วนเดียวกันไปเรื่อยๆ จนกว่าจะมีส่วนหัวข้อใหม่เกิดขึ้น ภายใต้หัวข้อ [dependencies] คุณจะได้ระบุข้อมูลให้ Cargo รับรู้ว่าโปรเจกต์ของคุณจำเป็นต้องพึ่งพาเครตภายนอกตัวใดบ้างและต้องการเวอร์ชันใดบ้าง ในที่นี้เราได้กำหนดให้ใช้เครต rand ด้วยการประกาศหมายเลขเวอร์ชัน 0.10.1 ทาง Cargo จะอ่านค่านี้ตามรูปแบบ การระบุเวอร์ชันเชิงความหมาย (Semantic Versioning) (หรือเรียกย่อๆ ว่า SemVer) ซึ่งเป็นมาตรฐานสากลที่นิยมใช้ในการเขียนระบุรุ่นของซอฟต์แวร์ การระบุว่า 0.10.1 จะหมายถึงค่าตัวย่อจากรูปเต็ม ^0.10.1 ซึ่งหมายความว่าจะอนุญาตให้ใช้งานเวอร์ชันใดก็ได้ที่มีค่าตั้งแต่ 0.10.1 ขึ้นไป แต่ต้องต่ำกว่ารุ่น 0.11.0

Cargo พิจารณาว่าเวอร์ชันในย่านดังกล่าวจะมีโครงสร้าง API สาธารณะที่ใช้งานเข้ากันได้ดีกับเวอร์ชัน 0.10.1 และการกำหนดกฎแบบนี้จะช่วยให้คุณได้รับรุ่นปรับปรุงล่าสุด (patch release) ที่ยังคอมไพล์เข้ากับโค้ดที่เราเขียนร่วมกันในบทนี้ได้ ส่วนเวอร์ชันรุ่น 0.11.0 ขึ้นไป จะไม่การันตีว่าจะยังมีรูปแบบ API ที่สอดคล้องกับตัวอย่างโค้ดที่จะใช้ถัดจากนี้ไป

คราวนี้โดยที่ยังไม่ต้องแก้ไขตัวโค้ดใดๆ เลย ให้ลองทำการคอมไพล์โปรเจกต์ด้วย Cargo ดังที่แสดงในรายการที่ 2-2:

$ cargo build
    Updating crates.io index
     Locking 8 packages to latest Rust 1.96.0 compatible versions
  Downloaded rand_core v0.10.1
  Downloaded chacha20 v0.10.1
  Downloaded rand v0.10.1
  Downloaded 3 crates (162.9KiB) in 0.59s
   Compiling libc v0.2.186
   Compiling rand_core v0.10.1
   Compiling getrandom v0.4.3
   Compiling cfg-if v1.0.4
   Compiling chacha20 v0.10.1
   Compiling rand v0.10.1
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.03s

คุณอาจเห็นผลลัพธ์ระบุหมายเลขเวอร์ชันที่แตกต่างไปจากนี้บ้างเล็กน้อย (แต่ขอยืนยันว่าพวกมันจะสามารถเข้ากันได้ดีกับโค้ดแน่นอนตามข้อตกลง SemVer!) รวมถึงอาจเห็นรูปแบบบรรทัดอื่นเพิ่มเติมตามประเภทของระบบปฏิบัติการ และลำดับแถวคำสั่งอาจจะสลับสับเปลี่ยนไปบ้าง

เมื่อเรานำเข้าไลบรารีภายนอกเข้ามาใช้งาน ทาง Cargo จะตรวจสอบและดึงเอาเวอร์ชันล่าสุดของเครตอื่นๆ ทั้งหมดที่เครตนี้จำเป็นต้องใช้เพิ่มเติมจากระบบฐานข้อมูลทะเบียนกลาง (registry) ซึ่งดึงข้อมูลจำลองมาจากเว็บไซต์ Crates.io ซึ่งเป็นพื้นที่แบ่งปันผลงานที่สมาชิกนักเขียนโค้ดในชุมชน Rust มารวมตัวกันแชร์โปรเจกต์โอเพนซอร์สของตนเพื่อให้คนอื่นๆ นำไปประยุกต์ใช้งาน

ภายหลังอัปเดตทะเบียนกลางแล้ว Cargo จะดำเนินการตรวจสอบในส่วนของ [dependencies] แล้วดาวน์โหลดเครตที่ระบุไว้หากเครื่องของคุณยังไม่ได้ทำการโหลดมันมาเก็บไว้ ในโปรเจกต์นี้แม้เราจะสั่งระบุ dependency ไว้เพียงตัวเดียวคือ rand แต่ในกระบวนการทำงาน Cargo ก็ได้ดึงข้อมูลเครตอื่นๆ อีกมากมายที่ rand ต้องพึ่งพามาให้โดยอัตโนมัติ หลังจากขั้นตอนการโหลดตัวเครตเสร็จแล้ว Rust จะดำเนินการคอมไพล์เครตพวกนี้เสียก่อน แล้วจึงคอมไพล์ตัวโปรเจกต์หลักของเราเข้ากับตัวไลบรารีดังกล่าว

หากคุณรีบรันคำสั่ง cargo build อีกครั้งทันทีโดยไม่มีการปรับแก้โค้ดเลย คุณจะไม่พบผลลัพธ์การทำงานใดๆ เลยนอกจากบรรทัดรายงาน Finished เนื่องจาก Cargo ทราบดีว่าไลบรารีที่ต้องใช้พึ่งพานั้นถูกดาวน์โหลดและประกอบไว้เรียบร้อยแล้ว และคุณก็ไม่ได้แก้ไข้อะไรเพิ่มเติมในไฟล์ Cargo.toml ทั้งหมดนี้รวมถึง Cargo ทราบว่าตัวโค้ดของคุณก็ยังไม่มีการเปลี่ยนแปลงใดๆ จึงไม่ต้องทำการคอมไพล์โค้ดใหม่ เมื่อไม่มีสิ่งใดค้างคาให้ทำระบบจึงเลือกจบการทำงานและปิดไป

หากคุณสลับมาเปิดไฟล์ src/main.rs แล้วทดลองแก้เนื้อหาเล็กๆ น้อยๆ แล้วบันทึกและสั่งสร้างใหม่อีกครั้ง คุณจะได้รับผลลัพธ์รายงานออกมาเพียงสองแถวเท่านั้น:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s

ผลลัพธ์ดังกล่าวชี้ให้เห็นว่า Cargo เลือกที่จะอัปเดตการประกอบโปรแกรมเฉพาะจุดตรงที่คุณได้เขียนแก้ไขไว้ในไฟล์ src/main.rs เท่านั้น เนื่องจากความสัมพันธ์กับเครตภายนอกอื่นๆ ยังคงเหมือนเดิม Cargo จึงตัดสินใจดึงโครงสร้างเดิมที่เคยดาวน์โหลดและคอมไพล์เสร็จสิ้นแล้วกลับมาใช้ซ้ำได้ทันที

การรับประกันการคอมไพล์ที่ให้ผลลัพธ์เหมือนเดิมทุกครั้ง (Reproducible Builds)

Cargo มีกลไกที่ช่วยรับประกันว่าตัวชิ้นงานผลลัพธ์ (artifact) ที่คอมไพล์ได้จากตัวโค้ดของคุณจะคงที่และเหมือนเดิมอยู่เสมอทุกๆ ครั้ง ไม่ว่าคุณหรือใครก็ตามจะเป็นผู้นำโค้ดนี้ไปสั่งสร้างใหม่ นั่นคือ Cargo จะบังคับล็อกการใช้งานรุ่นเวอร์ชันของไลบรารีต่างๆ ให้อยู่ตรงตามที่คุณเจาะจงไว้เดิมไปตลอดจนกว่าคุณจะสั่งการให้เปลี่ยนแปลง ยกตัวอย่างเช่น ในสัปดาห์หน้าเครต rand มีการออกรุ่นใหม่เวอร์ชัน 0.10.2 ซึ่งแม้จะช่วยแก้ไขข้อผิดพลาดที่สำคัญได้ แต่อาจเกิดปัญหาจุดอื่นที่อาจจะทำให้โปรแกรมของคุณทำงานล้มเหลว เพื่อหลีกเลี่ยงข้อกังวลดังกล่าว Rust จึงได้ทำการสร้างไฟล์ Cargo.lock ขึ้นมาในการรันคำสั่ง cargo build รอบแรกสุด ซึ่งคุณจะเห็นไฟล์ตัวนี้อยู่ในไดเรกทอรี guessing_game ของคุณเรียบร้อยแล้ว

ในตอนที่คุณเริ่มคอมไพล์สร้างโปรเจกต์เป็นครั้งแรก Cargo จะทำการประเมินค้นหารุ่นเวอร์ชันของไลบรารีทั้งหมดที่เข้าเกณฑ์และเขียนรายงานพวกนี้เก็บเข้าไว้ในไฟล์ Cargo.lock เมื่อใดก็ตามที่คุณสั่งการคอมไพล์โปรเจกต์นี้ในภายหลัง Cargo จะตรวจสอบว่าไฟล์ Cargo.lock นี้มีอยู่หรือไม่ และจะตรงเข้าไปหยิบค่าของรุ่นที่กำหนดในนั้นมาใช้งานทันทีโดยไม่ต้องเริ่มต้นประเมินคำนวณเวอร์ชันที่คู่ควรใหม่อีกรอบ สิ่งนี้ช่วยให้คุณมีการสร้างซอฟต์แวร์ที่ให้ผลลัพธ์คงที่อยู่เสมอตลอดไปโดยอัตโนมัติ กล่าวคือ โปรเจกต์ของคุณจะถูกล็อกไว้กับรุ่น 0.10.1 เสมอจนกว่าคุณจะสั่งอัปเกรดอย่างเป็นทางการ ด้วยประโยชน์จากไฟล์ Cargo.lock นี้ และเนื่องจากไฟล์นี้มีความสำคัญต่อการรักษารูปแบบดั้งเดิมของโปรเจกต์ มันจึงมักถูกนำเข้าสู่ระบบจัดการซอร์สโค้ด (source control) ร่วมกับโค้ดส่วนที่เหลือในโปรเจกต์ด้วย

การอัปเดตเครตเพื่อใช้เวอร์ชันใหม่กว่า

เมื่อคุณ มีความต้องการ จะสั่งอัปเกรดเครตจริง ๆ ทาง Cargo ก็มีเครื่องมือเตรียมไว้ให้ผ่านการใช้คำสั่ง update ซึ่งจะช่วยข้ามขั้นตอนการอ่านค่าล็อกเดิมในไฟล์ Cargo.lock และคำนวณหาเวอร์ชันรุ่นล่าสุดในปัจจุบันที่ยังเข้าข่ายเงื่อนไขที่คุณกำหนดไว้ในไฟล์ Cargo.toml จากนั้น Cargo จะนำรุ่นเวอร์ชันใหม่เหล่านั้นกลับไปแก้ไขบันทึกแทนที่ในไฟล์ Cargo.lock โดยตามขั้นตอนปกติทั่วไปแล้ว Cargo จะค้นหาช่วงรุ่นเวอร์ชันที่มากกว่า 0.10.1 แต่ต้องน้อยกว่า 0.11.0 เท่านั้น หากเครต rand มีการปล่อยอัปเดตเวอร์ชันออกมา 2 ตัว ได้แก่ 0.10.2 และ 0.999.0 แล้วคุณได้สั่งรัน cargo update คุณจะพบผลลัพธ์ปรากฏบนหน้าจอทำนองนี้:

$ cargo update
    Updating crates.io index
     Locking 1 package to latest Rust 1.96.0 compatible version
    Updating rand v0.10.1 -> v0.10.2 (available: v0.999.0)

Cargo เลือกที่จะเพิกเฉยต่อการออกรุ่น 0.999.0 และเมื่อตรวจดูในตอนหลัง คุณจะพบรายงานในไฟล์ Cargo.lock ปรับเปลี่ยนข้อมูลไปบันทึกว่าเวอร์ชันของเครต rand ที่เลือกใช้งานปัจจุบันนั้นเปลี่ยนมาเป็นรุ่น 0.10.2 หากคุณมีความประสงค์เจาะจงจะเรียกใช้เวอร์ชัน 0.999.0 หรือกลุ่มรุ่นตระกูล 0.999.x คุณจำเป็นต้องไปปรับแก้เนื้อหาในไฟล์ Cargo.toml ให้แสดงผลดังด้านล่างนี้แทน (แต่อย่าเพิ่งแก้ไขโปรเจกต์ของคุณจริงๆ ตอนนี้นะครับ เนื่องจากตัวอย่างอื่นๆ ด้านล่างจะอิงการทำงานกับเครต rand รุ่น 0.10 เป็นหลัก):

[dependencies]
rand = "0.999.0"

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

ยังมีข้อมูลและรายละเอียดที่น่าสนใจอีกมากมายเกี่ยวกับ Cargo และ ระบบนิเวศ (ecosystem) ของมัน ซึ่งเราจะกลับมาร่วมพูดคุยอย่างละเอียดในบทที่ 14 แต่สำหรับจุดนี้ข้อมูลเท่านี้ถือว่าพร้อมใช้แล้ว Cargo ช่วยให้นักพัฒนาสามารถเรียกนำโค้ดในรูปแบบไลบรารีกลับมาใช้ซ้ำได้อย่างไร้กังวล ส่งผลให้ชาว Rustacean สามารถเขียนโค้ดโครงการย่อย ๆ ที่เชื่อมประกอบเข้าหากันจากหลากหลายแพ็กเกจมารวมกันได้อย่างสะดวกโยธิน

การสร้างตัวเลขแบบสุ่ม

มาเริ่มทดลองดึงใช้เครต rand มาสร้างตัวเลขปริศนาให้เราทายกันเลย ขั้นตอนถัดมาคือให้เราเข้าไปปรับโครงสร้างของไฟล์ src/main.rs ดังที่แสดงในรายการที่ 2-3

use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

อันดับแรก เราจะเติมคำสั่ง use rand::prelude::*; เข้าไปที่หัวไฟล์ โดยที่โมดูล prelude นี้จะมีกลุ่มฟังก์ชันความสามารถส่วนที่ใช้สอยบ่อยๆ ของเครต rand บรรจุอยู่ และการใช้ use จะทำให้กลุ่มความสามารถดังกล่าวสามารถหยิบมาใช้ได้ทันทีภายใต้ขอบเขตการทำงานของโปรแกรมเรา

ถัดไปเราจะเพิ่มคำสั่งสองบรรทัดไว้ที่ส่วนตรงกลาง ในบรรทัดแรกจะเป็นการเรียกใช้ฟังก์ชัน rand::rng เพื่อส่งกลับตัวสุ่มตัวเลขที่เราต้องการใช้งาน นั่นคือตัวสุ่มที่จะแยกกันทำงานในแต่ละเธรด (local thread) และสร้างค่าตั้งต้นสำหรับการสุ่ม (seeded) โดยอิงความปลอดภัยจากระบบปฏิบัติการ หลังจากนั้นเราจึงเรียกใช้เมธอด random_range บนตัวสุ่มดังกล่าว เมธอดนี้ถูกนิยามผ่านความสามารถของ RngExt เทรต (trait) ซึ่งเป็นส่วนหนึ่งในโมดูล rand::prelude ที่เรานำเข้ามาในขอบเขตการทำงานของไฟล์ตั้งแต่หัวโปรแกรมตัวช่วย use rand::prelude::*; โดยที่เมธอด random_range จะรับนิพจน์อธิบายขอบเขตช่วงตัวเลข (range expression) เป็นอาร์กิวเมนต์เพื่อกำหนดว่าต้องการคำนวณตัวเลขสุ่มในย่านใด ซึ่งรูปแบบของนิพจน์กำหนดช่วงที่เราเลือกใช้งานนี้จะเขียนระบุในรูป ค่าเริ่มต้น..=ค่าสิ้นสุด ซึ่งมีผลครอบคลุมและนับรวมเอาทั้งค่าขอบเขตล่างและขอบเขตบนไปด้วย ดังนั้นเราจึงต้องสั่งให้ค้นหาค่าตัวเลขระหว่างช่วง 1..=100 เพื่อให้ได้ตัวสุ่มระหว่าง 1 ถึง 100

หมายเหตุ: คุณคงจะไม่มีทางล่วงรู้ได้โดยธรรมชาติว่าจำเป็นต้องนำเข้าสิ่งใดจากขอบเขตบ้าง หรือจะต้องเรียกใช้ฟังก์ชันและเมธอดตัวไหนจากเครตบ้าง ดังนั้นเครตแต่ละตัวจึงมาพร้อมคู่มืออธิบายการนำไปใช้งาน และอีกหนึ่งคุณสมบัติสุดเท่ของ Cargo ก็คือการที่คุณสามารถรันคำสั่ง cargo doc --open ซึ่งจะประกอบช่วยดึงคู่มือของ dependencies ทั้งหมดที่คุณดาวน์โหลดไว้มาสร้างเว็บเอกสารบนเครื่องและเปิดเปิดมันขึ้นมาแสดงบนเว็บเบราว์เซอร์ให้คุณทันที หากคุณสนใจและต้องการรู้เรื่องขีดความสามารถอื่น ๆ ของเครต rand ตัวอย่างเช่น ให้ทดลองสั่งรันคำสั่ง cargo doc --open แล้วเลือกคลิกที่คำว่า rand ในแถบด้านซ้ายมือได้เลย

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

ลองสลับมารันโปรแกรมดูสักสองสามครั้ง:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4

$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

คุณควรจะได้รับการสุ่มที่แสดงผลเลขหลังจากรันโปรแกรมในแต่ละรอบที่แตกต่างกัน และตัวเลขทั้งหมดนั้นควรอยู่ในช่วงตั้งแต่ 1 ถึง 100 หากคุณพบคำแจ้งเตือนความปลอดภัย (warnings) คุณสามารถเพิกเฉยได้ทันที และถ้าพบข้อผิดพลาดประการใด ให้ตรวจทานความถูกต้องว่าคุณใส่ rand = "0.10.1" ไว้ในไฟล์ Cargo.toml หรือไม่ เนื่องจากเครตเวอร์ชันสูงถัดไปอาจปรับเปลี่ยนโครงสร้าง API ไปจากนี้บ้าง แต่หากอยู่ในช่วงตระกูลรุ่น 0.10 ทั้งหมดจะสามารถใช้งานกับตัวอย่างโค้ดในบทเรียนนี้ได้เป็นอย่างดี

การเปรียบเทียบค่าที่ทายกับตัวเลขปริศนา

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

use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    // --snip--
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

อันดับแรก เราจะเพิ่มส่วนนำเข้าคำสั่ง use เข้าไปเพื่อเรียกใช้โครงสร้างประเภทข้อมูล std::cmp::Ordering จากในตัวคลังไลบรารีมาตรฐาน โดยข้อมูลประเภท Ordering นี้ก็ถือเป็นรูปแบบข้อมูล enum ประเภทหนึ่งที่มีตัวแปรรูปแบบ (variants) ทั้งหมดสามรุ่น ได้แก่ Less (น้อยกว่า), Greater (มากกว่า) และ Equal (เท่ากับ) ซึ่งนี่คือผลลัพธ์ทั้งสามแบบที่เป็นไปได้ในเชิงการตรวจสอบการเปรียบเทียบค่าสองค่า

หลังจากนั้นเราจึงเสริมโค้ดเพิ่มไปอีกห้าแถวด้านล่างสุดเพื่อประยุกต์ใช้งานประเภทข้อมูล Ordering โดยที่เมธอด cmp จะช่วยทำหน้าที่คำนวณเปรียบเทียบความสัมพันธ์ของค่า 2 ค่า ซึ่งเราจะสามารถเรียกใช้งานเมธอดนี้ได้บนตัวแปรใดๆ ก็ตามที่สามารถถูกประเมินชั่งเปรียบเทียบได้ โดยตัวมันจะรับค่าอ้างอิงของตัวเลขที่เราต้องการนำมาร่วมประเมิน ในที่นี้คือจะนำ guess ไปชั่งน้ำหนักกับ secret_number แล้วจึงคืนผลตอบรับกลับมาเป็นหนึ่งในรูปแบบ variants ของ enum Ordering ที่เราเรียกใช้ผ่าน use เมื่อครู่ จากนั้นเราจะใช้นิพจน์ควบคุม match ในการตัดสินใจทิศทางการดำเนินงานถัดไปด้วยการตรวจสอบรูปแบบของ Ordering ที่ได้มาจากการเปรียบเทียบ

นิพจน์ match จะถูกประกอบขึ้นมาจากตัวเลือกต่างๆ ที่เรียกว่า กิ่งการทำงาน (arms) โดยในแต่ละกิ่งจะระบุแนวทาง รูปแบบตัวอย่าง (pattern) เพื่อนำมาจับคู่วัดผล รวมถึงจะผูกโค้ดที่จะถูกทำงานเมื่อประเมินว่าค่าที่ส่งให้ match นั้นมีหน้าตาตรงกับเงื่อนไขรูปแบบของกิ่งดังกล่าว ซึ่ง Rust จะหยิบค่าต้นขั้วที่ส่งให้แก่ match มาเปรียบเทียบไล่ตรวจสอบทีละกิ่งตามลำดับ โครงสร้างเงื่อนไข Pattern และชุดคำสั่ง match จัดเป็นเครื่องมือที่มีประสิทธิภาพสูงของภาษา Rust ที่ช่วยให้นักพัฒนาอธิบายสภาวะการณ์ที่อาจเกิดขึ้นได้หลากหลายรูปแบบ และยังรับประกันว่าโค้ดจะดักสกัดครอบคลุมทุกเส้นทางที่อาจเกิดขึ้นได้ ซึ่งคุณลักษณะนี้จะได้รับคำอธิบายอย่างรอบด้านในบทที่ 6 และบทที่ 19 ตามลำดับ

ลองมาไล่สเต็ปทำความเข้าใจการทำงานนิพจน์ match ในกรณีตัวอย่างนี้ด้วยกัน สมมติว่าผู้ใช้งานกรอกคำตอบตัวเลขที่ทายเป็น 50 และระบบได้สุ่มค่าปริศนาสำหรับการทดสอบครั้งนี้ได้เลข 38

เมื่อโค้ดเริ่มเปรียบเทียบเลข 50 เข้ากับ 38 เมธอด cmp จะคืนค่ากลับมารายงานว่า Ordering::Greater เนื่องจาก 50 มีค่ามากกว่า 38 ทางนิพจน์ match จะนำเอาค่า Ordering::Greater ที่ได้รับนี้ไปตรวจเปรียบเทียบรูปแบบกับกิ่งตัวเลือกต่างๆ โดยเริ่มตรวจอันแรกคือ Ordering::Less ซึ่งพบว่าไม่สอดคล้องกันจึงเพิกเฉยไม่ประมวลผลโค้ดของกิ่งนั้นแล้วสลับไปทดสอบกิ่งต่อมา กิ่งถัดไปมีเงื่อนไขรูปแบบตรงกับ Ordering::Greater ซึ่งส่งผลให้เงื่อนไขตรงกันพอดิบพอดี! ดังนั้นระบบจึงรันโค้ดและพิมพ์คำว่า Too big! (ใหญ่เกินไป!) แสดงออกมาทางหน้าจอ นิพจน์ของ match จะยุติขบวนการทันทีหลังจากการหาคู่เปรียบเทียบแรกประสบความสำเร็จ ดังนั้นจึงไม่มีความจำเป็นต้องไล่ดูไปจนถึงกิ่งสุดท้ายในกรณีตัวอย่างรอบนี้

อย่างไรก็ตาม โค้ดในรายการที่ 2-4 นั้นยังไม่สามารถผ่านขั้นตอนคอมไพล์ได้ในตอนนี้ มาลองทดสอบกันเลย:

$ cargo build
   Compiling libc v0.2.186
   Compiling rand_core v0.10.1
   Compiling cfg-if v1.0.0
   Compiling getrandom v0.4.3
   Compiling chacha20 v0.10.1
   Compiling rand v0.10.1
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:23:21
   |
23 |     match guess.cmp(&secret_number) {
   |                 --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
   |                 |
   |                 arguments to this method are incorrect
   |
   = note: expected reference `&String`
              found reference `&{integer}`
note: method defined here
  --> /rustc/ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96/library/core/src/cmp.rs:999:7

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

หัวใจหลักของข้อความข้อผิดพลาดชี้ว่าเกิดปัญหารูปแบบข้อมูลไม่สอดคล้องกัน (mismatched types) ภาษา Rust ใช้ระบบตรวจสอบชนิดข้อมูลที่เข้มงวดและคงที่ (strong, static type system) แต่ขณะเดียวกันก็มีความสามารถในการคาดเดาชนิดข้อมูลได้เอง (type inference) ตอนที่เราเขียนโค้ดระบุ let mut guess = String::new() ภาษา Rust สามารถคาดคะเนได้ว่าตัวแปร guess ควรเป็นข้อมูลประเภท String โดยไม่ต้องสั่งให้นักเขียนโค้ดระบุตัวแปรลงไปชัดๆ ในทางกลับกัน ตัวแปร secret_number ถูกสร้างมาจากระบบสุ่มซึ่งคืนค่าเป็นตัวเลข ซึ่งชนิดข้อมูลตัวเลขของ Rust ที่จะรองรับค่าระหว่าง 1 ถึง 100 นั้นมีหลากหลายรูปแบบ เช่น i32 (เลขจำนวนเต็มมีเครื่องหมายขนาด 32 บิต), u32 (เลขจำนวนเต็มไม่มีเครื่องหมายขนาด 32 บิต), i64 (ตัวเลขขนาด 64 บิต) และอื่นๆ ซึ่งหากไม่ได้สั่งการใดๆ เพิ่มเติม Rust จะตั้งค่าเริ่มต้นให้ประเภทตัวเลขสุ่มดังกล่าวเป็นชนิดข้อมูล i32 เสมอ เว้นแต่เราจะไปช่วยป้อนข้อมูลชนิดที่จำเป็นในส่วนอื่นเพื่อช่วยให้ Rust เดาไปเป็นชนิดตัวเลขอื่นได้ และข้อผิดพลาดเกิดขึ้นก็เนื่องจาก Rust ปฏิเสธการชั่งน้ำหนักเปรียบเทียบข้อมูลต่างประเภทกัน นั่นคือระหว่างประเภทข้อความ (String) กับประเภทตัวเลข นั่นเอง

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

Filename: src/main.rs

use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    // --snip--

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

ซึ่งประโยคคำสั่งดังกล่าวคือ:

let guess: u32 = guess.trim().parse().expect("Please type a number!");

เราสร้างตัวแปรขึ้นมาตัวหนึ่งชื่อว่า guess แต่ช้าก่อน โปรแกรมของเราไม่ใช่ว่ามีตัวแปรชื่อ guess อยู่แล้วอย่างนั้นหรือ? ใช่ครับ แต่ระบบของภาษา Rust ยอมให้ผู้ใช้งานสร้างตัวแปรทับซ้อนชื่อเดิมที่ประกาศไปก่อนหน้านี้ได้ ซึ่งเรียกว่า การบังเงา (Shadowing) คุณลักษณะนี้ช่วยให้นักพัฒนาสามารถนำชื่อตัวแปรเก่ากลับมาใช้ซ้ำได้ แทนที่จะต้องมาตั้งชื่อตัวแปรแปลกแยกใหม่ขึ้นมา เช่น guess_str และ guess เป็นต้น เราจะไปดูรายละเอียดส่วนนี้กันใน บทที่ 3 สำหรับในตอนนี้ ให้จำเพียงว่าฟังก์ชันนี้มักถูกนำมาประยุกต์ใช้งานอย่างกว้างขวางเมื่อเราต้องการแปลงข้อมูลประเภทหนึ่งไปเป็นชนิดข้อมูลประเภทอื่น

เราผูกค่าตัวแปรตัวใหม่นี้เข้ากับนิพจน์ guess.trim().parse() โดยที่คำว่า guess ภายในนิพจน์จะไปดึงอ้างอิงค่ามาจากตัวแปร guess แรกเริ่มสุดที่ใช้เก็บค่าข้อความอินพุต เมธอด trim บนอินสแตนซ์ของ String จะจัดการล้างเครื่องหมายเว้นวรรคและเครื่องหมายขึ้นบรรทัดใหม่ทั้งหมดที่อยู่ขอบหน้าและขอบหลังข้อความ ซึ่งเป็นขั้นตอนที่สำคัญมากก่อนจะป้อนแปลงให้ข้อความดังกล่าวไปเป็นประเภทข้อมูล u32 (ซึ่งเก็บได้เฉพาะข้อมูลตัวเลขเพียวๆ เท่านั้น) เนื่องจากเวลาที่ผู้ใช้พิมพ์คำตอบแล้วกดปุ่ม enter เพื่อส่งผลลัพธ์ให้แก่ระบบของเมธอด read_line ตัวระบบจะบันทึกปุ่มอินพุตดังกล่าวแนบพ่วงท้ายข้อความมาด้วย ตัวอย่างเช่น หากผู้ใช้พิมพ์ตัวเลข 5 แล้วเคาะ enter ค่าตัวแปร guess จะหน้าตาเป็น 5\n โดยที่ \n คือเครื่องหมายขึ้นบรรทัดใหม่ (บนฝั่งระบบ Windows การกดแป้นพิมพ์ enter จะได้รับสัญลักษณ์พ่วงท้ายเป็น \r\n) เมธอด trim จะลบพวกสัญลักษณ์ \n หรือ \r\n เหล่านี้ทิ้งไปจนเหลือเพียงตัวเลขโดด 5

ส่วนเมธอด parse บนข้อความ จะมีหน้าที่เปลี่ยนประเภทข้อมูลข้อความไปเป็นข้อมูลรูปแบบอื่น ในที่นี้คือต้องการแปลงตัวอักษรให้กลายเป็นจำนวนเลข เรามีหน้าที่ชี้แนะให้ Rust ทราบความต้องการชนิดตัวเลขอย่างจำเพาะด้วยการพิมพ์ตัวบ่งชี้กำกับว่า let guess: u32 เครื่องหมายทวิภาค (:) หลังตัวแปร guess จะทำหน้าที่บอกกล่าวว่าจะทำการระบุชนิดข้อมูลให้ตัวแปรนี้ Rust เตรียมชนิดข้อมูลตัวเลขพื้นฐานไว้ให้เลือกใช้งานจำนวนหนึ่ง ตัวอย่างเช่น u32 ตรงจุดนี้คือจำนวนเต็มบวกไม่มีเครื่องหมายลบขนาด 32 บิต ซึ่งเหมาะสมมากสำหรับการใช้เป็นค่าตัวเลขบวกขนาดย่อม คุณจะได้ศึกษาข้อมูลประเภทตัวเลขอื่นใน บทที่ 3

และด้วยการใส่ป้ายกำกับชนิดข้อมูลชนิด u32 บนตัวแปรที่นำมาประเมินเปรียบเทียบกับ secret_number นี้เอง ทาง Rust ก็จะแอบอนุมานวิเคราะห์ความสัมพันธ์และแปลงค่าตัวแปร secret_number ให้รับรู้ว่าเป็นชนิดข้อมูล u32 ตามไปด้วยทันที ส่งผลให้ขั้นตอนชั่งตวงการเปรียบเทียบถัดไปนี้เป็นการคำนวณระหว่างตัวแปรชนิดประเภทตัวเลขขนาดยอดเดียวกันในที่สุด!

เมธอด parse จะยอมประมวลผลคำนวณสำเร็จได้เฉพาะกับตัวอักษรใด ๆ ที่สามารถนำมาประเมินค่าเป็นตัวเลขตามตรรกะได้เท่านั้น ด้วยเหตุนี้มันจึงสามารถเกิดข้อผิดพลาดขึ้นได้โดยง่าย ตัวอย่างเช่น หากข้อความอินพุตนำเข้ามาระบุเป็นสัญลักษณ์หน้าตาประหลาด เช่น A👍% ระบบจะไม่มีทางแปลงคำตอบนั้นมาเป็นตัวเลขจำนวนได้เลย และเนื่องจากกระบวนการนี้มีความเป็นไปได้ที่จะล้มเหลว เมธอด parse จึงส่งคืนผลลัพธ์เป็นข้อมูลประเภท Result สอดคล้องกับพฤติกรรมเดียวกับที่เมธอด read_line ทำงาน (ตามที่เราพูดคุยกันไว้ในหัวข้อ “การจัดการกับข้อผิดพลาดที่อาจเกิดขึ้นด้วย Result) เราจะจัดการกับค่าผลลัพธ์ Result ตัวนี้ในรูปแบบเดียวกันโดยเลือกพ่วงการเรียกใช้งานเมธอด expect อีกครั้ง หาก parse คืนผลกลับมาเป็น variant Err ของ Result เนื่องจากความล้มเหลวในการจัดกลุ่มแปลงข้อความ การสั่งรันคำสั่ง expect จะส่งผลให้โปรแกรมยุติทำงาน (crash) และรายงานข้อความที่เราส่งเข้าไป และในทางตรงกันข้ามหากเมธอด parse เปลี่ยนข้อความออกมาเป็นตัวเลขได้สมบูรณ์ มันจะรายงานค่าเป็น variant Ok ของข้อมูล Result และเมธอด expect จะแกะเอาตัวเลขจำนวนที่อยู่ภายในกล่อง Ok คืนกลับมาให้เรานำไปใช้งานได้ทันที

มาลองรันโปรแกรมดูตอนนี้กันเลย:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

ยอดเยี่ยม! แม้ว่าจะมีการเคาะเว้นวรรคเพิ่มเติมไปก่อนหน้าตัวเลขคำตอบ ตัวโปรแกรมก็ยังสามารถประเมินได้ถูกต้องว่าผู้ใช้งานป้อนค่าเลข 76 เข้ามา ให้ลองสลับรันโปรแกรมดูอีกสักสองสามรอบเพื่อประเมินความสอดคล้องของโปรแกรมตามสถานการณ์ต่างๆ เช่น ทายตัวเลขได้ตรงเฉลยพอดี, ทายตัวเลขที่สูงเกินไป และทายตัวเลขที่ต่ำเกินไป

เรามีส่วนการทำงานหลักของเกมทำงานได้เป็นส่วนใหญ่แล้ว แต่ผู้เล่นยังสามารถทายได้เพียงแค่รอบเดียวเท่านั้น เราลองมาใส่ลูปเพิ่มขีดความสามารถการทายหลายรอบกันดีกว่า!

การอนุญาตให้ทายได้หลายรอบด้วยโครงสร้างการวนซ้ำ

คำสั่งคำสำคัญ loop จะช่วยสร้างส่วนวนซ้ำไม่สิ้นสุด (infinite loop) ขึ้นมาใช้งาน เราจะเพิ่มมันเข้าไปในโปรแกรมเพื่อให้ผู้ใช้สามารถมีโอกาสแก้ตัวทายตัวเลขปริศนาได้เรื่อย ๆ:

Filename: src/main.rs

use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    // --snip--

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        // --snip--


        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse().expect("Please type a number!");

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

จากตัวอย่างโค้ดด้านบน จะสังเกตเห็นว่าเราได้โยกย้ายขั้นตอนการป้อนอินพุตทายตัวเลขเป็นต้นไปทั้งหมดเข้าไปใส่ไว้ภายใต้การครอบทำงานของ loop อย่าลืมจัดย่อหน้าเว้นวรรค (indent) สำหรับบรรทัดคำสั่งต่างๆ ที่อยู่ด้านใน loop เพิ่มเข้าไปอีกฝั่งละ 4 สเปซด้วย และสั่งรันตัวโปรแกรมใหม่อีกครั้ง คราวนี้โปรแกรมจะร้องขอตัวเลขทายไปเรื่อย ๆ ตลอดกาล ซึ่งจริงๆ แล้วพฤติกรรมนี้จะสร้างปัญหาปวดหัวชุดใหม่ให้เราตามมา เนื่องจากดูเหมือนว่าผู้ใช้จะไม่มีวิธีสั่งปิดการทำงานตัวเกมได้เลย!

โดยธรรมชาติแล้ว ผู้ใช้งานสามารถสั่งหยุดตัวโปรแกรมลงกะทันหันได้เสมอโดยการกดแป้นพิมพ์คีย์ลัด ctrl ร่วมกับ C แต่นอกเหนือจากนั้นก็ยังมีกลเม็ดวิธีเอาตัวรอดออกมาจากลูปนรกนี้ได้อยู่ ดังเช่นข้อมูลที่เราคุยกันในส่วนของเมธอด parse ในหัวข้อ [“การเปรียบเทียบค่าที่ทายกับตัวเลขปริศนา”](#comparing-the-guess to-the-secret-number) นั่นคือหากผู้ใช้อินพุตพิมพ์ข้อมูลแปลกปลอมที่ไม่ใช่ตัวเลขเข้ามา ตัวโปรแกรมจะทำการแครชปิดตัวลงไปเองโดยทันที ซึ่งเราสามารถนำเงื่อนไขลักษณะพิเศษจุดนี้มาใช้ประโยชน์เป็นคำสั่งช่วยทางเลือกออกจากการทำงานได้ ดังตัวอย่างนี้:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit

thread 'main' (6694925) panicked at src/main.rs:28:47:
Please type a number!: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

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

การหยุดการทำงานทันทีหลังจากทายถูก

เรามาสั่งการให้ตัวโปรแกรมหยุดรันตัวเกมเมื่อทายตัวเลขได้สำเร็จด้วยการเสริมชุดคำสั่ง break เข้าไปดังนี้:

Filename: src/main.rs

use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse().expect("Please type a number!");

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

การเพิ่มคำสั่ง break ต่อท้ายคำแสดงความยินดี You win! จะส่งผลให้โปรแกรมกระโดดหลุดออกมาจากขอบเขตการวนลูปในทันทีเมื่อทายเลขตรง และการหลุดจากขอบเขตวนลูปดังกล่าวจะทำให้โปรแกรมทำงานจบกระบวนการและปิดลงด้วย เนื่องจากตัวลูปนั้นถูกระบุไว้เป็นส่วนประกอบสุดท้ายของฟังก์ชัน main

การรับมือกับค่าข้อมูลป้อนเข้าไม่ถูกต้อง

เพื่อปรับพฤติกรรมของเกมให้มีความลื่นไหลเป็นมืออาชีพยิ่งขึ้น แทนที่เราจะปล่อยให้ตัวโปรแกรมปิดหน้าต่างหรือแครชตัวเองทิ้งเวลาที่ผู้เล่นพิมพ์ข้อมูลผิดหลงเข้ามา เราจะแก้ไขให้ระบบเพิกเฉยสัญลักษณ์เหล่านั้นและอนุญาตให้ผู้เล่นพิมพ์ทดสอบคำตอบใหม่ได้ทันทีเพื่อดำเนินการทายตัวเลขถัดไป เราสามารถจัดแจงแก้ไขแถวประโยคแปลตัวแปร guess จาก String ไปเป็น u32 ได้ดังที่แสดงตัวอย่างในรายการที่ 2-5

use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        // --snip--

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

เราสลับปรับแก้จากการใช้คำสั่ง expect เดิม มาปรับเรียกใช้คำสั่งผ่านประโยคนิพจน์ควบคุม match เพื่อหลีกเลี่ยงกระบวนการปิดโปรแกรมเนื่องจากเกิดข้อผิดพลาด และผันตัวมารับมือจัดแจงปัญหาด้วยระบบจัดการความปลอดภัย จำไว้ว่าเมธอด parse จะส่งผลคืนข้อมูลเป็นชนิด Result ซึ่งเป็น enum ที่ประกอบสร้าง variant สำคัญ 2 รุ่นคือ Ok และ Err เราใช้คำสั่งตรวจจับ match ในจุดนี้เช่นเดียวกับที่ประยุกต์ใช้งานไปแล้วกับผลลัพธ์ข้อมูล Ordering ของเมธอด cmp ก่อนหน้านี้

หากเมธอด parse ทำงานแปลงตัวอักษรเป็นค่าจำนวนเต็มบวกเสร็จสิ้นสมบูรณ์ มันจะคืนค่าส่งกลับมาในห่อ variant ชื่อ Ok ซึ่งบรรจุเลขผลลัพธ์ที่แปลงได้อยู่ข้างใน และห่อ Ok ดังกล่าวจะมีรูปแบบเงื่อนไขพฤติกรรมไปตรงตามเงื่อนไขของกิ่งแรก Ok(num) ส่งผลให้นิพจน์ match แกะเอามูลค่าแท้จริงของ num ที่ parse คำนวณได้ส่งต่อออกมา และค่าตัวเลขจำนวนดังกล่าวจะถูกนำไปบันทึกระบุตรงตัวแปร guess ใหม่ที่เราเตรียมประกาศค่าไว้นั่นเอง

และในทำนองกลับกัน หากเมธอด parse ประสบปัญหาไม่สามารถปรับเปลี่ยนข้อความให้ไปเป็นข้อมูลตัวเลขจำนวนได้ มันจะคืนค่ารายงานมาเป็นตัวแปรรูปแบบ Err ซึ่งบรรจุประวัติข้อมูลข้อผิดพลาดพ่วงมาด้วย ซึ่งค่า Err ดังกล่าวจะประเมินแล้วไม่ตรงกับเงื่อนไขในกิ่ง Ok(num) กิ่งแรก แต่จะจับคู่ลงล็อกพอดิบพอดีกับกิ่งเงื่อนไขตัวเลือกที่สองคือ Err(_) โดยเครื่องหมายขีดล่าง _ ทำหน้าที่เป็นตัวแปรแบบดักจับทั้งหมด (catch-all value) ซึ่งในกรณีตัวอย่างนี้เราประกาศเพื่อสื่อว่าต้องการดักจับเงื่อนไขข้อมูลผิดพลาด Err ทุกรูปแบบไม่ว่าประวัติความล้มเหลวข้างในจะระบุเป็นข้อมูลประการใดก็ตาม ซึ่งตัวโปรแกรมจะดำเนินการตามโค้ดของกิ่งที่สองนั่นคือการเรียกใช้ฟังก์ชัน continue ซึ่งสั่งให้โปรแกรมข้ามขบวนการที่เหลือลงไปและย้อนเวลากลับกระโดดขึ้นไปเริ่มรันวนรอบใหม่ของโครงสร้าง loop ทันทีและร้องขออินพุตตัวเลขทายต่อไป ส่งผลลัพธ์ในทางปฏิบัติจริงให้ตัวโปรแกรมเพิกเฉยข้ามข้อผิดพลาดทั้งหมดที่เครื่องมือ parse มีโอกาสพบนั่นเอง!

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

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

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

use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

และตอนนี้ คุณก็ได้สร้างโปรแกรมเกมทายตัวเลขด้วยตนเองได้สำเร็จเรียบร้อยแล้ว ขอแสดงความยินดีด้วยครับ!

สรุป

โปรเจกต์นี้ถือเป็นบทเรียนในเชิงลงมือปฏิบัติที่ช่วยแนะนำตัวอย่างโครงสร้างและแนวคิดสำคัญของภาษา Rust ไว้มากมาย ไม่ว่าจะเป็น let, match, ฟังก์ชัน, วิธีการเรียกใช้งานเครตภายนอก และรายละเอียดอื่นๆ ซึ่งในบทเรียนถัดๆ ไปข้างหน้า คุณจะได้มีโอกาสเจาะลึกศึกษาข้อมูลเหล่านี้อย่างกระจ่างและเป็นระบบยิ่งขึ้น โดยที่บทที่ 3 จะครอบคลุมแนวคิดพื้นฐานเรื่องตัวแปร, ชนิดประเภทข้อมูล และฟังก์ชันที่ภาษาส่วนใหญ่มีร่วมกัน รวมถึงวิธีใช้ใน Rust บทที่ 4 จะเริ่มพาไปรู้จักกับคุณสมบัติความเป็นเจ้าของสิทธิ์การครอบครองหน่วยความจำ (ownership) ซึ่งเป็นฟีเจอร์เด่นอันเป็นเอกลักษณ์ที่ทำให้ภาษา Rust แตกต่างจากภาษาอื่นๆ บทที่ 5 จะแนะนำเรื่องโครงสร้างอ็อบเจกต์ (structs) และรูปแบบเมธอด และบทที่ 6 จะอธิบายถึงการทำงานของ enums