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, เมธอด, associated function, external crate และอื่น ๆ! ในบทถัด ๆ ไป เราจะสำรวจไอเดียเหล่านี้ในรายละเอียดมากขึ้น ในบทนี้คุณแค่ฝึก พื้นฐาน

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

Setup โปรเจกต์ใหม่

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

$ cargo new guessing_game
$ cd guessing_game

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

ดูไฟล์ Cargo.toml ที่ถูก generate:

Filename: Cargo.toml

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

[dependencies]

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

Filename: src/main.rs

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

ทีนี้มา compile โปรแกรม “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 มีประโยชน์เมื่อคุณต้องทำซ้ำในโปรเจกต์อย่างรวดเร็ว อย่างที่เรา จะทำในเกมนี้ ทดสอบแต่ละ iteration อย่างเร็วก่อนไปทำต่อ

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

ประมวลผลคำตอบ

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

Filename: 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}");
}
Listing 2-1: โค้ดที่รับคำตอบจากผู้ใช้แล้วพิมพ์ออกมา

โค้ดนี้มีข้อมูลเยอะ ฉะนั้นมาอธิบายทีละบรรทัด ในการรับ input จากผู้ใช้แล้ว พิมพ์ผลออกมา เราต้องนำ library io (input/output) เข้า scope library io มาจาก standard library ที่รู้จักในชื่อ 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}");
}

โดย default Rust มีชุดของ item ที่ประกาศใน standard library ซึ่งถูกนำเข้า scope ของทุกโปรแกรม ชุดนี้เรียกว่า prelude และคุณดูทุกอย่างในมันได้ ใน documentation ของ standard library

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

อย่างที่คุณเห็นในบทที่ 1 ฟังก์ชัน main เป็น entry point ของโปรแกรม:

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}");
}

Syntax fn ประกาศฟังก์ชันใหม่ วงเล็บ () บ่งบอกว่าไม่มี parameter และ curly bracket { เริ่ม body ของฟังก์ชัน

อย่างที่คุณเรียนในบทที่ 1 ด้วย println! เป็น macro ที่พิมพ์ string ออก หน้าจอ:

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}");
}

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

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

ขั้นต่อไป เราจะสร้าง ตัวแปร เพื่อเก็บ input ของผู้ใช้ ดังนี้:

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}");
}

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

let apples = 5;

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

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

หมายเหตุ: syntax // เริ่ม comment ที่ดำเนินไปจนจบบรรทัด Rust ละเว้น ทุกอย่างใน comment เราจะพูดถึง comment ในรายละเอียดเพิ่มเติมใน บทที่ 3

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

Syntax :: ในบรรทัด ::new บ่งบอกว่า new เป็น associated function ของ type String associated function คือฟังก์ชันที่ implement บน type ใน กรณีนี้คือ String ฟังก์ชัน new นี้สร้าง string ว่างใหม่ คุณจะพบ ฟังก์ชัน new ใน type หลายตัว เพราะมันเป็นชื่อที่ใช้บ่อยสำหรับฟังก์ชัน ที่สร้างค่าใหม่บางอย่าง

โดยสรุป บรรทัด let mut guess = String::new(); ได้สร้างตัวแปร mutable ที่ ตอนนี้ถูก bind กับ instance ว่างใหม่ของ String เฮ้อ!

รับ input จากผู้ใช้

จำได้ว่าเรา include functionality สำหรับ input/output จาก standard library ด้วย use std::io; ในบรรทัดแรกของโปรแกรม ทีนี้เราจะเรียกฟังก์ชัน stdin จาก module io ซึ่งจะให้เราจัดการ input จากผู้ใช้:

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}");
}

ถ้าเราไม่ได้ import module io ด้วย use std::io; ที่ต้นโปรแกรม เราก็ยัง ใช้ฟังก์ชันได้โดยเขียนการเรียกฟังก์ชันนี้เป็น std::io::stdin ฟังก์ชัน stdin return instance ของ std::io::Stdin ซึ่ง เป็น type ที่แทน handle ของ standard input ของ terminal คุณ

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

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

จัดการ failure ที่อาจเกิดขึ้นด้วย Result

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

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");

อย่างไรก็ตาม บรรทัดยาว ๆ บรรทัดเดียวอ่านยาก ทางที่ดีที่สุดคือแบ่งมัน บ่อย ครั้งที่ฉลาดในการแนะนำ newline และ whitespace อื่น ๆ เพื่อช่วยแยกบรรทัด ยาว ๆ เมื่อคุณเรียกเมธอดด้วย syntax .method_name() ทีนี้มาพูดถึงสิ่งที่ บรรทัดนี้ทำ

อย่างที่กล่าวไว้ก่อนหน้า read_line ใส่สิ่งที่ผู้ใช้ป้อนลงใน string ที่ เราส่งให้ แต่มันยัง return ค่า Result ด้วย Result เป็น enumeration ที่มักเรียกว่า enum ซึ่งเป็น type ที่อยู่ในสถานะหนึ่งจากหลายสถานะที่เป็นไปได้ เราเรียกแต่ละสถานะที่ เป็นไปได้ว่า variant

บทที่ 6 จะครอบคลุม enum ในรายละเอียดเพิ่มเติม จุด ประสงค์ของ type Result เหล่านี้คือ encode ข้อมูลการจัดการ error

variant ของ Result คือ Ok และ Err variant Ok บ่งบอกว่า operation สำเร็จ และมีค่าที่ generate สำเร็จอยู่ข้างใน variant Err หมายความว่า operation ล้มเหลว และมีข้อมูลเกี่ยวกับวิธีหรือเหตุผลที่ operation ล้มเหลว

ค่าของ type Result เช่นเดียวกับค่าของ type ใด ๆ มีเมธอดที่ประกาศไว้บน มัน instance ของ Result มี expect method ที่ คุณเรียกได้ ถ้า instance ของ Result นี้เป็นค่า Err expect จะทำให้ โปรแกรม crash และแสดงข้อความที่คุณส่งเป็น argument ให้ expect ถ้าเมธอด read_line return Err มันน่าจะเป็นผลของ error ที่มาจาก OS เบื้องล่าง ถ้า instance ของ Result นี้เป็นค่า Ok expect จะเอาค่าที่ Ok เก็บ ไว้ แล้ว return แค่ค่านั้นให้คุณ เพื่อให้คุณใช้ได้ ในกรณีนี้ ค่านั้นคือ จำนวน byte ใน input ของผู้ใช้

ถ้าคุณไม่เรียก expect โปรแกรมจะ compile ได้ แต่คุณจะได้ 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)]` 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 ที่ return จาก read_line บ่ง บอกว่าโปรแกรมยังไม่ได้จัดการ error ที่อาจเกิดขึ้น

วิธีที่ถูกในการ suppress warning คือเขียนโค้ดจัดการ error จริง ๆ แต่ใน กรณีของเรา เราแค่อยากให้โปรแกรมนี้ crash เมื่อเกิดปัญหา เราจึงใช้ expect ได้ คุณจะเรียนเรื่อง recover จาก error ใน บทที่ 9

พิมพ์ค่าด้วย placeholder ของ println!

นอกจาก curly bracket ปิด เหลือแค่บรรทัดเดียวที่ต้องพูดถึงในโค้ดที่ผ่านมา:

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}");
}

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

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

println!("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

ณ จุดนี้ ส่วนแรกของเกมเสร็จแล้ว — เรารับ input จาก keyboard แล้วพิมพ์มัน ออกมา

Generate ตัวเลขลับ

ต่อไป เราต้อง generate ตัวเลขลับที่ผู้ใช้จะพยายามทาย ตัวเลขลับควรต่างกันทุก ครั้ง เพื่อให้เกมสนุกที่จะเล่นมากกว่าหนึ่งครั้ง เราจะใช้ตัวเลขสุ่มระหว่าง 1 ถึง 100 เพื่อให้เกมไม่ยากเกินไป Rust ยังไม่มี functionality สำหรับ random number ใน standard library อย่างไรก็ตาม ทีม Rust จัดให้มี crate rand ที่มี functionality ดังกล่าว

เพิ่ม functionality ด้วย crate

จำได้ว่า crate คือชุดของไฟล์ source code ของ Rust โปรเจกต์ที่เรา build อยู่ เป็น binary crate ซึ่งเป็น executable crate rand เป็น library crate ซึ่ง มีโค้ดที่ตั้งใจให้ใช้ในโปรแกรมอื่น และ execute เองไม่ได้

การ coordinate external crate ของ Cargo เป็นจุดที่ Cargo เปล่งประกายจริง ๆ ก่อนที่เราจะเขียนโค้ดที่ใช้ rand เราต้องแก้ไฟล์ Cargo.toml เพื่อรวม crate rand เป็น dependency เปิดไฟล์นั้นแล้วเพิ่มบรรทัดต่อไปนี้ที่ด้านล่าง ภายใต้ section header [dependencies] ที่ Cargo สร้างให้คุณ อย่าลืมระบุ rand ให้เป๊ะตามที่เราใส่ไว้ ด้วย version นี้ มิฉะนั้นตัวอย่างโค้ดใน tutorial นี้อาจไม่ทำงาน:

Filename: Cargo.toml

[dependencies]
rand = "0.8.5"

ในไฟล์ Cargo.toml ทุกอย่างที่ตามมาหลัง header เป็นส่วนหนึ่งของ section นั้น ที่ดำเนินไปจนกระทั่ง section อื่นเริ่ม ใน [dependencies] คุณบอก Cargo ว่าโปรเจกต์ของคุณพึ่งพา external crate ตัวไหน และต้องการ version ไหน ของ crate เหล่านั้น ในกรณีนี้ เราระบุ crate rand ด้วย semantic version specifier 0.8.5 Cargo เข้าใจ Semantic Versioning (บางครั้งเรียกว่า SemVer) ซึ่งเป็นมาตรฐานในการเขียนเลข version specifier 0.8.5 จริง ๆ แล้วเป็น shorthand ของ ^0.8.5 ซึ่งหมายถึง version ใด ๆ ที่ อย่างน้อย 0.8.5 แต่ต่ำกว่า 0.9.0

Cargo ถือว่า version เหล่านี้มี API สาธารณะที่เข้ากันได้กับ version 0.8.5 และ specification นี้รับประกันว่าคุณจะได้ patch release ล่าสุดที่ยัง compile กับโค้ดในบทนี้ได้ version 0.9.0 ขึ้นไปไม่รับประกันว่าจะมี API เดียวกับที่ตัวอย่างต่อไปนี้ใช้

ทีนี้ โดยไม่เปลี่ยนโค้ดใด ๆ มา build โปรเจกต์ ตามที่แสดงใน Listing 2-2

$ cargo build
  Updating crates.io index
   Locking 15 packages to latest Rust 1.85.0 compatible versions
    Adding rand v0.8.5 (available: v0.9.0)
 Compiling proc-macro2 v1.0.93
 Compiling unicode-ident v1.0.17
 Compiling libc v0.2.170
 Compiling cfg-if v1.0.0
 Compiling byteorder v1.5.0
 Compiling getrandom v0.2.15
 Compiling rand_core v0.6.4
 Compiling quote v1.0.38
 Compiling syn v2.0.98
 Compiling zerocopy-derive v0.7.35
 Compiling zerocopy v0.7.35
 Compiling ppv-lite86 v0.2.20
 Compiling rand_chacha v0.3.1
 Compiling rand v0.8.5
 Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
  Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.48s
Listing 2-2: output จากการรัน cargo build หลังเพิ่ม crate rand เป็น dependency

คุณอาจเห็นเลข version ต่างกัน (แต่ทั้งหมดจะเข้ากันได้กับโค้ด ขอบคุณ SemVer!) และบรรทัดต่าง ๆ ก็ต่างกัน (ขึ้นกับ OS) และบรรทัดอาจอยู่ในลำดับต่างกัน

เมื่อเรารวม external dependency Cargo จะ fetch version ล่าสุดของทุกอย่างที่ dependency นั้นต้องการ จาก registry ซึ่งเป็นสำเนาข้อมูลจาก Crates.io Crates.io เป็นที่ที่คนใน ecosystem ของ Rust post โปรเจกต์ Rust แบบ open source ให้คนอื่นใช้

หลัง update registry แล้ว Cargo เช็ค section [dependencies] แล้ว download crate ใด ๆ ที่ list ไว้ที่ยังไม่ได้ download ในกรณีนี้ แม้เราจะ list แค่ rand เป็น dependency Cargo ก็ดึง crate อื่น ๆ ที่ rand พึ่งพาเพื่อทำงาน มาด้วย หลัง download crate แล้ว Rust compile พวกมัน แล้ว compile โปรเจกต์ ที่มี dependency พร้อมใช้

ถ้าคุณรัน cargo build อีกครั้งทันทีโดยไม่เปลี่ยนอะไร คุณจะไม่ได้ output ใด ๆ นอกจากบรรทัด Finished Cargo รู้ว่ามัน download และ compile dependency แล้ว และคุณไม่ได้เปลี่ยนอะไรเกี่ยวกับพวกมันในไฟล์ Cargo.toml Cargo ยัง รู้ว่าคุณไม่ได้เปลี่ยนอะไรเกี่ยวกับโค้ดของคุณ ดังนั้นมันไม่ recompile โค้ด ด้วย เมื่อไม่มีอะไรให้ทำ มันก็แค่ออก

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

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

บรรทัดเหล่านี้แสดงว่า Cargo update build ตามการเปลี่ยนเล็ก ๆ ของคุณในไฟล์ src/main.rs เท่านั้น dependency ของคุณไม่ได้เปลี่ยน Cargo จึงรู้ว่ามันใช้ ของที่ download และ compile ไว้แล้วซ้ำได้

รับประกันการ build ที่ทำซ้ำได้

Cargo มีกลไกที่รับประกันว่าคุณ rebuild artifact เดียวกันได้ทุกครั้งที่คุณ หรือใครก็ตาม build โค้ดของคุณ — Cargo จะใช้แค่ version ของ dependency ที่ คุณระบุ จนกว่าคุณจะบอกเป็นอย่างอื่น เช่น สมมติว่าสัปดาห์หน้า version 0.8.6 ของ crate rand ออกมา และ version นั้นมี bug fix สำคัญ แต่ก็มี regression ที่จะทำให้โค้ดของคุณพัง ในการจัดการเรื่องนี้ Rust สร้างไฟล์ Cargo.lock ครั้งแรกที่คุณรัน cargo build ตอนนี้เราจึงมีไฟล์นี้ใน directory guessing_game

เมื่อคุณ build โปรเจกต์ครั้งแรก Cargo หา version ของ dependency ทั้งหมดที่ ตรงเกณฑ์ แล้วเขียนลงไฟล์ Cargo.lock เมื่อคุณ build โปรเจกต์ในอนาคต Cargo จะเห็นว่าไฟล์ Cargo.lock มีอยู่ และจะใช้ version ที่ระบุที่นั่น แทนการทำงานหา version อีกครั้ง สิ่งนี้ให้คุณมี build ที่ทำซ้ำได้อัตโนมัติ พูดอีกอย่าง โปรเจกต์ของคุณจะอยู่ที่ 0.8.5 จนกว่าคุณจะ upgrade แบบ explicit ขอบคุณไฟล์ Cargo.lock เพราะไฟล์ Cargo.lock สำคัญสำหรับ build ที่ทำซ้ำ ได้ มันจึงมักถูก check in เข้า source control พร้อมโค้ดที่เหลือในโปรเจกต์ ของคุณ

Update crate เพื่อได้ version ใหม่

เมื่อคุณ อยาก update crate Cargo มีคำสั่ง update ซึ่งจะละเว้นไฟล์ Cargo.lock แล้วหา version ล่าสุดทั้งหมดที่ตรง specification ใน Cargo.toml จากนั้น Cargo จะเขียน version เหล่านั้นลงไฟล์ Cargo.lock มิฉะนั้น โดย default Cargo จะมองหาแค่ version ที่มากกว่า 0.8.5 และต่ำกว่า 0.9.0 ถ้า crate rand release version ใหม่สอง version คือ 0.8.6 และ 0.999.0 คุณจะเห็นสิ่งต่อไปนี้ถ้ารัน cargo update:

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

Cargo ละเว้น release 0.999.0 ณ จุดนี้ คุณจะสังเกตเห็นการเปลี่ยนแปลงในไฟล์ Cargo.lock ระบุว่า version ของ crate rand ที่คุณใช้ตอนนี้คือ 0.8.6 ถ้าจะใช้ rand version 0.999.0 หรือ version ใด ๆ ใน series 0.999.x คุณ ต้อง update ไฟล์ Cargo.toml ให้หน้าตาเป็นแบบนี้แทน (อย่าทำการเปลี่ยนนี้ จริง ๆ เพราะตัวอย่างต่อไปนี้สมมติว่าคุณใช้ rand 0.8):

[dependencies]
rand = "0.999.0"

ครั้งถัดไปที่คุณรัน cargo build Cargo จะ update registry ของ crate ที่มี อยู่ และประเมิน requirement rand ของคุณใหม่ตาม version ใหม่ที่คุณระบุ

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

Generate ตัวเลขสุ่ม

มาเริ่มใช้ rand เพื่อ generate ตัวเลขให้ทาย ขั้นต่อไปคือ update src/main.rs ตามที่แสดงใน Listing 2-3

Filename: src/main.rs
use std::io;

use rand::Rng;

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

    let secret_number = rand::thread_rng().gen_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}");
}
Listing 2-3: เพิ่มโค้ดเพื่อ generate ตัวเลขสุ่ม

ขั้นแรก เราเพิ่มบรรทัด use rand::Rng; trait Rng ประกาศเมธอดที่ random number generator implement และ trait นี้ต้องอยู่ใน scope เราถึงใช้เมธอด เหล่านั้นได้ บทที่ 10 จะครอบคลุม trait ในรายละเอียด

ถัดไป เราเพิ่มสองบรรทัดตรงกลาง ในบรรทัดแรก เราเรียกฟังก์ชัน rand::thread_rng ที่ให้ random number generator เฉพาะที่เราจะใช้: ตัวที่ local กับ thread ของการ execute ปัจจุบัน และถูก seed โดย OS แล้วเราเรียก เมธอด gen_range บน random number generator เมธอดนี้ถูกประกาศโดย trait Rng ที่เรานำเข้า scope ด้วย statement use rand::Rng; เมธอด gen_range รับ range expression เป็น argument แล้ว generate ตัวเลขสุ่มใน range นั้น range expression ที่เราใช้ที่นี่อยู่ในรูป start..=end และ inclusive ทั้ง ขอบเขตล่างและบน เราจึงต้องระบุ 1..=100 เพื่อขอตัวเลขระหว่าง 1 ถึง 100

หมายเหตุ: คุณคงไม่รู้เองว่าจะใช้ trait ตัวไหน และเรียกเมธอดและฟังก์ชัน ไหนจาก crate ดังนั้นแต่ละ crate จึงมี documentation พร้อมคำแนะนำการใช้ งาน อีกฟีเจอร์เจ๋ง ๆ ของ Cargo คือการรันคำสั่ง cargo doc --open จะ build documentation ที่ dependency ทั้งหมดของคุณให้มา และเปิดมันใน browser ของคุณ ถ้าคุณสนใจ functionality อื่นใน crate rand เช่น รัน cargo doc --open แล้วคลิก rand ใน sidebar ทางซ้าย

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

ลองรันโปรแกรมหลาย ๆ ครั้ง:

$ 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 ดีมาก!

เปรียบเทียบคำตอบกับตัวเลขลับ

ตอนนี้เรามี input ของผู้ใช้และตัวเลขสุ่มแล้ว เราเปรียบเทียบมันได้ ขั้นตอน นั้นแสดงใน Listing 2-4 หมายเหตุว่าโค้ดนี้ยัง compile ไม่ได้ ดังที่เราจะ อธิบาย

Filename: src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::Rng;

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

    let secret_number = rand::thread_rng().gen_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!"),
    }
}
Listing 2-4: จัดการค่าที่ return จากการเปรียบเทียบตัวเลขสองตัว

ขั้นแรก เราเพิ่ม statement use อีกตัว นำ type ชื่อ std::cmp::Ordering เข้า scope จาก standard library type Ordering เป็น enum อีกตัว และมี variant Less, Greater และ Equal นี่คือสามผลลัพธ์ที่เป็นไปได้เมื่อ คุณเปรียบเทียบสองค่า

จากนั้น เราเพิ่มห้าบรรทัดใหม่ที่ด้านล่างที่ใช้ type Ordering เมธอด cmp เปรียบเทียบสองค่า และเรียกได้บนอะไรก็ตามที่เปรียบเทียบได้ มันรับ reference ของอะไรก็ตามที่คุณต้องการเปรียบเทียบด้วย: ที่นี่ มันเปรียบเทียบ guess กับ secret_number จากนั้นมัน return variant ของ enum Ordering ที่เรานำเข้า scope ด้วย statement use เราใช้ match expression เพื่อตัดสินใจว่าจะทำอะไรต่อ ขึ้นกับว่า variant ไหนของ Ordering ถูก return จากการเรียก cmp ด้วยค่าใน guess และ secret_number

match expression ประกอบด้วย arm arm ประกอบด้วย pattern ที่จะ match กับ และโค้ดที่ควรรันถ้าค่าที่ให้ match ตรงกับ pattern ของ arm นั้น Rust เอาค่าที่ให้ match แล้วดูผ่าน pattern ของแต่ละ arm ตามลำดับ pattern และ โครงสร้าง match เป็นฟีเจอร์ที่ทรงพลังของ Rust — มันให้คุณแสดงสถานการณ์ที่ หลากหลายที่โค้ดของคุณอาจเจอ และทำให้แน่ใจว่าคุณจัดการทั้งหมด ฟีเจอร์เหล่านี้ จะครอบคลุมในรายละเอียดในบทที่ 6 และ 19 ตามลำดับ

มาเดินผ่านตัวอย่างกับ match expression ที่เราใช้ที่นี่ สมมติว่าผู้ใช้ทาย 50 และตัวเลขลับที่ generate แบบสุ่มครั้งนี้คือ 38

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

อย่างไรก็ตาม โค้ดใน Listing 2-4 ยัง compile ไม่ได้ ลองดู:

$ cargo build
   Compiling libc v0.2.86
   Compiling getrandom v0.2.2
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.10
   Compiling rand_core v0.6.2
   Compiling rand_chacha v0.3.0
   Compiling rand v0.8.5
   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/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/cmp.rs:979:8

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

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

สุดท้าย เราอยากแปลง String ที่โปรแกรมอ่านเป็น input ให้เป็น type ตัวเลข เพื่อให้เปรียบเทียบเชิงตัวเลขกับตัวเลขลับได้ เราทำโดยการเพิ่มบรรทัดนี้เข้า ใน body ของฟังก์ชัน main:

Filename: src/main.rs

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

use rand::Rng;

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

    let secret_number = rand::thread_rng().gen_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 ช่วยให้เรา shadow ค่าก่อนหน้าของ guess ด้วยค่า ใหม่ Shadowing ให้เราใช้ชื่อตัวแปร guess ซ้ำ แทนที่จะถูกบังคับให้สร้าง ตัวแปรไม่ซ้ำกันสองตัว เช่น guess_str และ guess เราจะครอบคลุมเรื่องนี้ ในรายละเอียดเพิ่มเติมใน บทที่ 3 แต่ตอนนี้ รู้ว่าฟีเจอร์นี้มักใช้เมื่อคุณต้องการแปลงค่าจาก type หนึ่งเป็นอีก type หนึ่ง

เรา bind ตัวแปรใหม่นี้กับ expression guess.trim().parse() guess ใน expression อ้างถึงตัวแปร guess ตัวเดิมที่มี input เป็น string เมธอด trim บน instance ของ String จะกำจัด whitespace ใด ๆ ที่ต้นและท้าย ซึ่ง เราต้องทำก่อนที่จะแปลง string เป็น u32 ที่มีได้แต่ข้อมูลตัวเลข ผู้ใช้ ต้องกด enter เพื่อตอบสนอง read_line และป้อนคำตอบ ซึ่งเพิ่ม อักขระ newline เข้าไปใน string เช่น ถ้าผู้ใช้พิมพ์ 5 และกด enter guess จะหน้าตาเป็นแบบนี้: 5\n \n แทน “newline” (บน Windows การกด enter ให้ผลเป็น carriage return และ newline, \r\n) เมธอด trim กำจัด \n หรือ \r\n ออก เหลือแค่ 5

เมธอด parse บน string แปลง string ให้เป็น type อื่น ที่นี่ เราใช้มันแปลงจาก string เป็นตัวเลข เราต้องบอก Rust ถึง type ตัวเลขที่เราต้องการแบบเป๊ะ ๆ โดยใช้ let guess: u32 colon (:) หลัง guess บอก Rust ว่าเราจะ annotate type ของตัวแปร Rust มี type ตัวเลข built-in ไม่กี่ตัว u32 ที่เห็นที่นี่คือจำนวนเต็ม 32-bit แบบไม่มี เครื่องหมาย เป็น default choice ที่ดีสำหรับตัวเลขบวกขนาดเล็ก คุณจะเรียน type ตัวเลขอื่น ๆ ใน บทที่ 3

นอกจากนี้ การ annotate u32 ในตัวอย่างโปรแกรมนี้ และการเปรียบเทียบกับ secret_number หมายความว่า Rust จะ infer ว่า secret_number ควรเป็น u32 ด้วย ดังนั้นตอนนี้การเปรียบเทียบจะเป็นระหว่างค่าสองค่าที่มี type เดียวกัน!

เมธอด parse จะทำงานได้แค่บนอักขระที่เชิง logic แปลงเป็นตัวเลขได้ จึงทำ ให้เกิด error ได้ง่าย ถ้า เช่น string มี A👍% ก็ไม่มีทางแปลงเป็นตัวเลข เพราะมันอาจล้มเหลว เมธอด parse จึง return type Result เช่นเดียวกับ เมธอด read_line (พูดถึงก่อนหน้านี้ใน “จัดการ failure ที่อาจเกิดขึ้นด้วย Result) เราจะปฏิบัติต่อ Result นี้แบบเดียวกันโดยใช้เมธอด expect อีกครั้ง ถ้า parse return variant Err ของ Result เพราะมันสร้างตัวเลขจาก string ไม่ได้ การเรียก expect จะ crash เกมและพิมพ์ข้อความที่เราให้มัน ถ้า parse แปลง string เป็นตัวเลขสำเร็จ มันจะ return variant Ok ของ Result และ expect จะ return ตัวเลขที่เราต้องการจากค่า 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!

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

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

ให้ทายหลายครั้งด้วย loop

keyword loop สร้าง infinite loop เราจะเพิ่ม loop เพื่อให้ผู้ใช้มีโอกาส ทายตัวเลขมากกว่า:

Filename: src/main.rs

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

use rand::Rng;

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

    let secret_number = rand::thread_rng().gen_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!"),
        }
    }
}

อย่างที่เห็น เราย้ายทุกอย่างตั้งแต่ prompt input คำตอบลงไปใน loop อย่าลืม indent บรรทัดภายใน loop เพิ่มสี่ space และรันโปรแกรมอีกครั้ง โปรแกรมตอนนี้ จะถามคำตอบใหม่ตลอดไป ซึ่งจริง ๆ ก็เกิดปัญหาใหม่ ดูเหมือนผู้ใช้ออกไม่ได้!

ผู้ใช้สามารถ interrupt โปรแกรมได้เสมอด้วย keyboard shortcut ctrl-C แต่ยังมีอีกวิธีหนึ่งในการหนีจากสัตว์ประหลาด ที่ไม่รู้จักอิ่มนี้ ดังที่กล่าวไว้ในการพูดถึง parse ใน “เปรียบเทียบคำตอบกับตัวเลขลับ” — ถ้าผู้ใช้ป้อนคำตอบที่ไม่ใช่ตัวเลข โปรแกรมจะ crash เราใช้ประโยชน์จากสิ่ง นั้นเพื่อให้ผู้ใช้ออกได้ ดังที่แสดงที่นี่:

$ 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' 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 จะออกจากเกม แต่อย่างที่คุณจะสังเกต การป้อน input ที่ไม่ใช่ ตัวเลขใด ๆ ก็จะทำเหมือนกัน นี่ไม่ดีที่สุด พูดน้อย ๆ — เราอยากให้เกมหยุดเมื่อ ทายตัวเลขถูกด้วย

ออกเมื่อทายถูก

มา program ให้เกมออกเมื่อผู้ใช้ชนะ โดยเพิ่ม statement break:

Filename: src/main.rs

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

use rand::Rng;

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

    let secret_number = rand::thread_rng().gen_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! ทำให้โปรแกรมออกจาก loop เมื่อผู้ใช้ ทายตัวเลขลับถูก การออกจาก loop ก็หมายถึงการออกจากโปรแกรม เพราะ loop เป็น ส่วนสุดท้ายของ main

จัดการ input ที่ไม่ถูกต้อง

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

Filename: src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::Rng;

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

    let secret_number = rand::thread_rng().gen_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;
            }
        }
    }
}
Listing 2-5: ละเว้นคำตอบที่ไม่ใช่ตัวเลข และขอคำตอบใหม่แทนการ crash โปรแกรม

เราเปลี่ยนจากการเรียก expect มาเป็น match expression เพื่อย้ายจากการ crash บน error มาเป็นการจัดการ error จำได้ว่า parse return type Result และ Result เป็น enum ที่มี variant Ok และ Err เราใช้ match expression ที่นี่ เหมือนที่เราทำกับผล Ordering ของเมธอด cmp

ถ้า parse แปลง string เป็นตัวเลขสำเร็จ มันจะ return ค่า Ok ที่มีตัวเลข ผลลัพธ์อยู่ ค่า Ok นั้นจะ match pattern ของ arm แรก และ match expression ก็จะแค่ return ค่า num ที่ parse produce และใส่ใน Ok ตัว เลขนั้นจะลงเอยตรงที่เราต้องการพอดี ในตัวแปร guess ใหม่ที่เรากำลังสร้าง

ถ้า parse ไม่ สามารถแปลง string เป็นตัวเลข มันจะ return ค่า Err ที่ มีข้อมูลเพิ่มเติมเกี่ยวกับ error ค่า Err ไม่ match pattern Ok(num) ใน arm match แรก แต่มัน match pattern Err(_) ใน arm ที่สอง underscore _ คือค่าจับทั้งหมด ในตัวอย่างนี้ เราบอกว่าเราอยาก match ค่า Err ทั้งหมด ไม่ว่าจะมีข้อมูลอะไรอยู่ข้างใน ดังนั้นโปรแกรมจะ execute โค้ดของ arm ที่สอง continue ซึ่งบอกโปรแกรมให้ไปที่ iteration ถัดไปของ loop และขอคำตอบใหม่ จึงทำให้โปรแกรมละเว้น error ทั้งหมดที่ 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! ที่ output ตัวเลขลับ Listing 2-6 แสดงโค้ดสุดท้าย

Filename: src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::Rng;

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

    let secret_number = rand::thread_rng().gen_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;
            }
        }
    }
}
Listing 2-6: โค้ดเกมทายตัวเลขที่สมบูรณ์

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

สรุป

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