เขียนเกมทายตัวเลข
มากระโดดเข้าสู่ 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
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 จากผู้ใช้แล้ว
พิมพ์ผลออกมา เราต้องนำ 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
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
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}");
}
ขั้นแรก เราเพิ่มบรรทัด 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 อื่นใน craterandเช่น รัน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 ไม่ได้ ดังที่เราจะ อธิบาย
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!"),
}
}
ขั้นแรก เราเพิ่ม 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
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;
}
}
}
}
เราเปลี่ยนจากการเรียก 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 แสดงโค้ดสุดท้าย
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;
}
}
}
}
ณ จุดนี้ คุณ build เกมทายตัวเลขสำเร็จแล้ว ขอแสดงความยินดี!
สรุป
โปรเจกต์นี้เป็นวิธีลงมือทำเพื่อแนะนำแนวคิด Rust ใหม่ ๆ ให้คุณ: let,
match, ฟังก์ชัน, การใช้ external crate และอื่น ๆ ในบทถัด ๆ ไป คุณจะเรียน
รู้เกี่ยวกับแนวคิดเหล่านี้ในรายละเอียดเพิ่มเติม บทที่ 3 ครอบคลุมแนวคิดที่
ภาษาโปรแกรมส่วนใหญ่มี เช่น ตัวแปร, ชนิดข้อมูล และฟังก์ชัน และแสดงวิธีใช้
ใน Rust บทที่ 4 สำรวจ ownership ฟีเจอร์ที่ทำให้ Rust ต่างจากภาษาอื่น บทที่
5 พูดถึง struct และ syntax ของเมธอด และบทที่ 6 อธิบายวิธีที่ enum ทำงาน