การเขียนโปรแกรม เกมทายตัวเลข
ก้าวเข้าสู่ Rust ด้วยการทำโปรเจกต์ในภาคปฏิบัติกัน!
ในบทนี้จะแนะนำให้คุณรู้จักกับแนวคิดทั่วไปของ Rust
โดยแสดงให้คุณเห็นถึงวิธีใช้งานในโปรแกรมจริง
คุณจะได้เรียนรู้เกี่ยวกับ let
, match
, method ที่เกี่ยวข้องกับฟังก์ชั่น,
crate และอื่น ๆ ! ในบทต่อไปนี้เราจะสำรวจแนวคิดเหล่านี้ให้ละเอียดยิ่งขึ้น
ในบทนี้คุณจะเพียงฝึกฝนพื้นฐานเท่านั้น
เราจะใช้เกมทายตัวเลข ซึ่งเป็นปัญหาการเขียนโปรแกรมคลาสสิกสำหรับผู้เริ่มต้น วิธีการทำงาน: โปรแกรมจะสุ่มตัวเลขจำนวนเต็มขึ้นมา โดยอยู่ในระหว่าง 1 ถึง 100 จากนั้นจะแจ้งให้ผู้เล่นทาย หลังจากป้อนตัวเลขที่ได้ทายแล้ว โปรแกรมจะระบุว่าตัวเลขที่ทายนั้นต่ำหรือสูงเกินไป หากทายถูก เกมจะแสดงข้อความแสดงความยินดี และออกจากโปรแกรม
สร้างโปรเจกต์ใหม่
ในการสร้างโปรเจกต์ใหม่ ไปที่โฟลเดอร์ projects ที่คุณได้สร้างไว้เมื่อบทที่ 1 และสร้างโปรเจกต์ใหม่โดยใช้ Cargo ดังต่อไปนี้:
$ cargo new guessing_game
$ cd guessing_game
คำสั่งแรก cargo new
ใช้ชื่อของโปรเจกต์ (guessing_game
) เป็น argument แรก
คำสั่งที่สองย้ายไปที่โฟลเดอร์ของโปรเจกต์ใหม่
ดูไฟล์ Cargo.toml ทีได้สร้างขึ้น:
ชื่อไฟล์: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
อย่างที่คุณเห็นไปในบทที่ 1 ซึ่ง cargo new
ได้สร้างโปรแกรม “Hello, world!” ให้คุณ
ตรวจสอบไฟล์ src/main.rs:
ชื่อไฟล์: 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 1.50s
Running `target/debug/guessing_game`
Hello, world!
คำสั่ง run
มีประโยชน์เมื่อคุณจำเป็นต้องทำซ้ำอย่างรวดเร็ว (rapidly 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 เราจำเป็นต้องนำเข้าไลบารรี input/output io
เข้ามามาในขอบเขต โดยไลบรารี 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 คุณสามารถดูรายละเอียดทั้งหมดได้ ในเอกสารคู่มือไลบรารีมาตรฐาน
หากสิ่งที่คุณต้องการใช้ไม่อยู่ใน prelude คุณต้องนำเข้าสิ่งนั้นมาไว้ยังขอบเขตที่ชัดเจน
ด้วยคำสั่ง use
การใช้ไลบรารี std::io
จะมอบคุณสมบัติมากมายให้กับคุณ
รวมถึงความสามารถในการรับ input จากผู้ใช้
อย่างที่คุณได้เห็นในบทที่ 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
จะประกาศฟังก์ชั่นใหม่; วงเล็บ ()
เป็นตัวระบุว่าไม่มี parameters;
และวงเล็บปีกกาเปิด {
เป็นจุดเริ่มต้นภายในฟังก์ชั่น
อย่างที่คุณได้เรียนรู้ในบทที่ 1 println!
คือ macro ซึ่งสามารถแสดงข้อความออกทางหน้าจอ:
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);
}
โค้ดนี้จะแสดงผลข้อความที่แจ้งว่าเกมอะไรและขอให้ผู้ใช้ป้อนข้อมูล
การจัดเก็บค่าด้วยตัวแปร
ถัดไป เราจะสร้าง ตัวแปร ที่เก็บค่าจากผู้ใช้ที่ป้อนเข้ามา ดังต่อไปนี้:
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
และกำหนดค่าเป็น 5 โดยใน Rust ตัวแปรจะมีค่าเริ่มต้นเป็น immutable ซึ่งหมายถึง
หลังจากที่คุณกำหนดค่าให้กับตัวแปรในครั้งแรกแล้ว ค่าจะไม่เปลี่ยนแปลง
เราจะกล่าวถึงรายละเอียดเพิ่มเติมเกี่ยวกับแนวคิดนี้ในหัวข้อ “ตัวแปรและความไม่แน่นอน” ของบทที่ 3
และในการสร้างตัวแปร mutable เราจะเพิ่ม mut
ไว้หน้าชื่อตัวแปร:
let apples = 5; // immutable
let mut bananas = 5; // mutable
หมายเหตุ: ไวยากรณ์
//
หมายถึงจุดเริ่มต้นของ comment ยาวไปจนจบบรรทัด ซึ่ง Rust จะเพิกเฉยกับอะไรก็ตามที่อยู่ใน comment เราจะกล่าวถึงรายละเอียดเพิ่มเติมเกี่ยวกับ comment ใน บทที่ 3
กลับมาที่เกมทายตัวเลข ตอนนี้คุณรู้แล้วว่า let mut guess
จะสร้างตัวแปร mutable ชื่อ guess
ส่วนเครื่องหมายเท่ากับ (=
) เป็นการบอก Rust ว่าเราต้องการกำหนดค่าบางอย่างให้กับตัวแปร
ทางด้านขวาของเครื่องหมายเท่ากับคือค่าที่กำหนดให้กับตัวแปร guess
ซึ่งก็คือผลลัพธ์ที่ได้จากการเรียกใช้
String::new
ที่ return อินสแตนซ์ใหม่ของ String
โดย String
ที่กล่าวถึงนี้คือประเภท string ที่อยู่ในไลบรารีมาตรฐาน ที่ซึ่งสามารถขยายขนาดได้
และข้อความถูกเข้ารหัสในรูปแบบ UTF-8
ไวยากรณ์ ::
ในบรรทัด ::new
บงชี้ว่า new
เป็นฟังก์ชั่นที่เกี่ยวข้องกับประเภท String
โดย ฟังก์ชั่นที่เกี่ยวข้อง หมายถึงฟังก์ชั่นที่ถูกนำไปใช้กับประเภทตัวแปรนั้น ๆ หรือในกรณีนี้ก็คือ String
และฟังก์ชั่น new
ได้สร้าง string เปล่าอันใหม่ขึ้นมา
คุณจะพบฟังก์ชั่น new
ในหลายประเภทของตัวแปร
เนื่องจากเป็นชื่อสามัญสำหรับฟังก์ชั่นที่สร้างค่าใหม่ขึ้นมา
คำอธิบายเต็มคือ บรรทัด let mut guess = String::new();
ได้สร้างตัวแปร mutable
ที่ถูกกำหนดค่าเป็นอินสแตนซ์ใหม่ของ String
; หวีว!
การรับ input จากผู้ใช้
จำได้ว่าเรานำเข้าฟังก์ชั่น input/output จากไลบรารีมาตรฐานด้วย use std::io;
ในบรรทัดแรกของโปรแกรม ตอนนี้เราจะเรียกใช้ฟังก์ชั่น stdin
จากโมดูล 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);
}
หากเราไม่ได้นำเข้าไลบรารี io
ด้วย use std::io;
ที่จุดเริ่มต้นของโปรแกรม
เรายังคงสามารถใช้งานฟังก์ชั่นได้ โดยการเรียกใช้ฟังก์ชั่นแบบนี้ std::io::stdin
ฟังก์ชั่น stdin
จะ return อินสแตนซ์ของ std::io::Stdin
ซึ่งก็คือประเภทของตัวแปรที่สามารถรับ input มาตรฐานสำหรับเทอร์มินัลของคุณ
ถัดไป บรรทัด .read_line(&mut guess)
จะเรียกใช้ method read_line
จากการจัดการ input มาตรฐานเพื่อรับ input จากผู้ใช้ นอกจากนี้เรายังส่ง &mut guess
เป็น argument
ไปที่ read_line
เพื่อบอกว่าข้อความจาก input ของผู้ใช้จะเก็บที่ตัวแปรนี้
งานทั้งหมดของ read_line
คือการนำสิ่งที่ผู้ใช้พิมพ์ไปเป็น input มาตรฐาน และผนวกเข้ากับ string
(โดยไม่ต้องเขียนทับเนื้อหา) ดังนั้นเราจึงสิ่งตัวแปร string ไปเป็น argument
และ argument นั้นต้องเป็น mutable เพื่อให้ method สามารถเปลี่ยนแปลงข้อความใน argument ได้
อักขระ &
บ่งชี้ว่า argument นั้นเป็น reference ซึ่งช่วยให้คุณอนุญาตให้โค้ดหลาย ๆ ส่วน
สามารถเข้าถึงข้อมูลจำนวนหนึ่งโดยไม่จำเป็นต้องคัดลอกข้อมูลนั้นลงในหน่วยความจำหลายครั้ง
reference เป็นคุณสมบัติที่ซับซ้อน และเป็นข้อดีหลัก ๆ ข้อหนึ่งของ Rust ที่ช่วยให้ปลอดภัยและง่ายต่อการใช้ reference
คุณไม่จำเป็นต้องรู้รายละเอียดมากมายเพื่อเขียนโปรแกรมนี้ให้สำเร็จ สำหรับตอนนี้ ทั้งหมดที่คุณจำเป็นต้องรู้คือ
reference มีค่าเริ่มต้นเป็น immutable เช่นเดียวกับตัวแปร ดังนั้นคุณต้องเขียน
&mut guess
แทน &guess
เพื่อทำให้มันเป็น mutable (บทที่ 4 จะอธิบาย reference ให้ละเอียดมากขึ้น)
การจัดการกับความล้มเหลวที่อาจจะเกิดขึ้นด้วย Result
เรายังคงไปกันต่อกับโค้ดในบรรทัดนี้ ขณะนี้เรากำลังกพูดถึงข้อความบรรทัดที่สาม แต่โปรดทราบว่าโค้ดสามบรรทัดนี้เป็นส่วนหนึ่งส่วนเดียวกัน ส่วนถัดไปนี้คือ method:
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
ด้วยไวยากรณ์ .method_name()
ตอนนี้เรามาคุยกันว่าบรรทัดนี้ทำอะไร
ตามที่ได้กล่าวไปในข้างต้น read_line
จะใส่ค่าอะไรก็ตามที่ผู้ใช้ป้อนเข้ามาลงไปในตัวแปร string
ที่เราใส่เข้ามา แตก็จะ return ค่า Result
ด้วย
โดย Result
ก็คือ enumeration หรือโดยทัวไปเรียกว่า enum
ซึ่งเป็นประเภทของตัวแปรที่สามารถเป็นค่าใดค่าหนึ่ง ในหลาย ๆ ค่าที่เป็นไปได้
เราเรียกแต่ละค่าที่เป็นไปได้ว่า variant
บทที่ 6 จะคลอบคลุมถึงรายละเอียดเพิ่มเติมเกี่ยวกับ enum
วัตถุประสงค์ของประเภทตัวแปร Result
คือการเข้ารหัสข้อมูลเพื่อจัดการข้อผิดพลาด
variant ของ Result
คือ Ok
และ Err
โดย variant Ok
บงชี้ว่าการดำเนินการสำเร็จ
และภายใน Ok
คือค่าที่สร้างสำเร็จ variant Err
หมายถึงการดำเนินการล้มเหลว
และ Err
ประกอบด้วยข้อมูลที่เกี่ยวกับวิธีการหรือสาเหตุที่การดำเนินการล้มเหลว
ค่าของประเภทตัวแปร Result
ก็เหมือนกับประเภทตัวแปรอื่น ๆ ที่มี method ด้วยเช่นกัน
อินสแตนซ์ของ Result
มี expect
method
ที่คุณสามารถเรียกใช้ได้ หากอินสแตนซ์ของ Result
นี้คือค่า Err
ดังนั้น expect
จะทำให้โปรแกรมขัดข้อง และแสดงผลข้อความที่คุณใส่เป็น argument ไว้ใน expect
หาก method read_line
มีการ return ค่าเป็น Err
มีแนวโน้มว่าจะมีสาเหตุมาจากระบบปฏิบัติการพื้นฐาน
หากอินสแตนซ์ของ Result
คือค่า Ok
expect
จะดึงค่าที่ Ok
เก็บไว้ออกมา และ return เฉพาะค่านั้น เพื่อให้คุณสามารถใช้งานได้
ซึ่งในกรณีนี้ค่านั้นคือจำนวนของ byte ที่ถูกใช้ใน input ของผู้ใช้
หากคุณไม่เรียกใช้ expect
โปรแกรมจะยังคงสามารถคอมไพล์ได้ แต่คุณจะได้รับคำเตือน:
$ 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
ซึ่งระบุว่าโปรแกรมไม่ได้จัดการกับข้อผิดพลาดที่อาจเกิดขึ้น
วิธีที่ถูกต้องในการจัดการกับคำเตือนคือการเขียนโค้ดจัดการกับข้อผิดพลาด
แต่ในกรณีของเรา เราเพียงต้องการให้โปรแกรมนี้หยุดทำงานเมื่อเกิดปัญหา
ดังนั้นเราจึงสามารถใช้ expect
ได้ คุณจะได้เรียนรู้เกี่ยวกับการกู้คืนจากข้อผิดพลาดใน
บทที่ 9
การแสดงผลค่าด้วย 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);
}
บรรทัดนี้จะทำการแสดงผลข้อความที่ประกอบไปด้วย input ของผู้ใช้
ชุดเครื่องหมายปีกกา {}
คือตัวระบุตำแหน่ง: ให้ค้ดว่า {}
เป็นเหมือนคีมจับปูขนาดเล็กที่ยึดค่าต่าง ๆ ไว้กับที่
เมื่อต้องการแสดงค่าของตัวแปร สามารถใส่ชื่อตัวแปรลงไปในวงเล็บปีกกาได้
เมื่อต้องการแสดงผลลัพธ์จากการดำเนินการ ใส่วงเล็บปีกกาเปล่าลงในชุดข้อความ
จากนั้นตามด้วยจุลภาค (,
) ที่คั่นระหว่างตัวดำเนินการต่าง ๆ ตามลำดับเดียวกับวงเล็บปีกกา
การแสดงผลค่าตัวแปรและผลการดำเนินการ โดยเรียกใช้ 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 [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
ในตอนนี้ ส่วนแรกของเกมก็เสร็จเรียบร้อยแล้ว เรากำลังรับ input จากคีย์บอร์ดจากนั้นก็แสดงผลออกมา
สร้างตัวเลขที่เป็นความลับ
ถัดไป เราจำเป็นต้องสร้างตัวเลขลับเพื่อให้ผู้ใช้พยายามทายให้ถูก ตัวเลขลับควรแตกต่างกันในแต่ละครั้งที่เล่นเกม
เพื่อให้เกมสนุกยิ่งขึ้นหากเล่นหลายครั้ง เราจะใช้ตัวเลขสุ่มระหว่าง 1 ถึง 100 เพื่อให้เกมไม่ยากเกินไป
Rust ไม่มีฟังก์ชั่นสุ่มเลขในไลบรารีมาตรฐาน อย่างไรก็ตามทีมงาน Rust ได้จัดเตรียม crate rand
ที่มีฟังก์ชั่นดังกล่าวไว้แล้ว
การใช้ crate เพื่อเพิ่มฟังก์ชั่นการใช้งาน
โปรดจำไว้ว่า crate นั้นคือชุดของไฟล์ซอร์สโค้ดของ Rust
โปรเจกต์ที่เรากำลังสร้างคือ crate ไบนารี ซึ่งก็คือไฟล์ที่สามารถรันได้
ส่วน crate rand
ก็คือ crate ไลบรารี ซึ่งบรรจุโค้ดที่ตั้งใจให้ใช้กับโปรแกรมอื่น ๆ และไม่สามารถรันได้ด้วยตัวเอง
การประสานงานของ Cargo กับ crate ภายนอกคือจุดที่ Cargo โดดเด่นจริง ๆ
ก่อนที่เราจะเขียนโค้ดโดยใช้ rand
เราจำเป็นต้องแก้ไขไฟล์ Cargo.toml
เพื่อเพิ่ม crate rand
ไปยัง dependency โดยทำการเปิดไฟล์ดังกล่าวขึ้นมา
และเพิ่มบรรทัดต่อไปนี้ที่ด้านล่าง ใต้ส่วนหัว [dependencies]
ที่ Cargo ได้สร้างให้คุณ
ตรวจสอบให้แน่ใจว่าได้ระบุ rand
แบบเดียวกับที่เราระบุไว้ที่นี่ โดยใช้เวอร์ชั่นเดียวกันนี้
มิฉะนั้น ตัวอย่างโค้ดในบทช่วยสอนนี้อาจใช้การไม่ได้:
Filename: Cargo.toml
[dependencies]
rand = "0.8.5"
ในไฟล์ Cargo.toml ทุกสิ่งทุกอย่างที่ตามหลังส่วนหัวจะเป็นส่วนหนึ่งของส่วนหัว
ต่อเนื่องไปจนกว่าจะเริ่มส่วนหัวอื่น ๆ โดยใน [dependencies]
คุณได้แจ้งให้ Cargo ทราบว่า crate
ภายนอกที่โปรเจกต์ของคุณจำเป็นต้องใช้และเวอร์ชั่นที่เกี่ยวข้องมีอะไรบ้าง
ในกรณีนี้ เราระบุ crate rand
พร้อมกับเวอร์ชั่นเชิงความหมาย 0.8.5
โดย Cargo เข้าใจการกำหนดเวอร์ชั่นเชิงความหมาย (บางครั้งเรียกว่า SemVer)
ซึ่งเป็นมาตรฐานในการเขียนหมายเลขเวอร์ชั่น การระบุ 0.8.5
จริง ๆ แล้วคือรูปย่อของ ^0.8.5
ซึ่งหมายถึงเวอร์ชั่น 0.8.5 หรือใหม่กว่า แต่ต้องต่ำกว่า 0.9.0 ลงมา
Cargo ถือว่าเวอร์ชั่นเหล่านี้มี API สาธารณะที่เข้ากันได้กับเวอร์ชั่น 0.8.5 และข้อกำหนดนี้รับประกันว่าคุณจะยังได้รับแพตช์เวอร์ชั่นล่าสุด โดยที่ยังสามารถคอมไพล์กับโค้ดในบทนี้ได้ แต่ไม่รับประกันว่าเวอร์ชั่น 0.9.0 หรือสูงกว่าจะมี API เดียวกันกับตัวอย่างต่อไปนี้:
ตอนนี้ เรามาคอมไพล์โปรเจกต์โดยยังไม่ต้องแก้ไขโค้ดใด ๆ ดังที่แสดงในรายการ 2-2
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
Downloaded libc v0.2.127
Downloaded getrandom v0.2.7
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.16
Downloaded rand_chacha v0.3.1
Downloaded rand_core v0.6.3
Compiling libc v0.2.127
Compiling getrandom v0.2.7
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
คุณอาจจะเห็นหมายเลขเวอร์ชั่นที่ต่างกัน (แต่ทั้งหมดจะเข้ากันได้กับโค้ด ขอบคุณ SemVer!) และบรรทัดที่ต่างกัน (ขึ้นอยู่กับระบบปฏิบัติการ) และบรรทัดอาจจะอยู่ในลำดับที่ต่างกัน
เมื่อเรานำเข้า dependency ภายนอก Cargo จะทำการดึงทุกสิ่งทุกอย่างในเวอร์ชั่นล่าสุดที่ dependency ต้องการ จาก registry ซึ่งเป็นสำเนาของข้อมูลจาก Crates.io Crates.io เป็นที่ที่ผู้คนในระบบนิเวศของ Rust โพสต์โปรเจกต์ Rust โอเพ่นซอร์ส เพื่อให้คนอื่น ๆ ได้ใช้งาน
หลังจากอัปเดต registry แล้ว Cargo จะตรวจสอบส่วน [dependencies]
และดาวน์โหลดรายการ crate ใดก็ตามที่ยังไม่ได้ดาวน์โหลด
ในกรณีนี้ แม้ว่าเราจะระบุ rand
เป็น dependency Cargo ก็ยังดึงเอา crate อื่น ๆ ที่
rand
ต้องการมาด้วย หลังจากดาวน์โหลด crate มาแล้ว Rust จะคอมไพล์ crate ทั้งหมดนี้
จากนั้นจึงจะคอมไพล์โปรเจกต์ด้วย dependency ที่มีอยู่
หากคุณรัน cargo build
อีกครั้งทันที โดยไม่ได้เปลี่ยนแปลงอะไร
คุณจะไม่ได้ผลลัพธ์ใด ๆ นอกจากบรรทัด Finished
เพราะ Cargo
ทราบว่าได้ดาวน์โหลด และคอมไพล์ dependency เรียบร้อยแล้ว
และคุณไม่ได้เปลี่ยนแปลงอะไรเกี่ยวกับ dependency ในไฟล์ Cargo.toml ของคุณ
Cargo ยังทราบอีกว่าคุณไม่ได้เปลี่ยนแปลงอะไรเกี่ยวกับโค้ดของคุณ
ดังนั้นจึงไม่จำเป็นต้องคอมไพล์ซ้ำอีกครั้ง เมื่อไม่มีอะไรต้องทำมันจึงแค่จบการทำงาน
หากคุณเปิดไฟล์ src/main.rs ทำการเปลี่ยนแปลงเล็กน้อยและบันทึก จากนั้นทำการคอมไพล์อีกครั้ง คุณจะเห็นเพียงผลลัพธ์สองบรรทัดดังนี้:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
บรรทัดเหล่านี้แสดงให้เห็นว่า Cargo นั้นจะทำการคอมไพล์เพียงโค้ดของคุณเท่านั้น ซึ่งได้มีการเปลี่ยนแปลงเล็กน้อยในไฟล์ src/main.rs ในขณะที่ dependency ของคุณไม่มีการเปลี่ยนแปลง Cargo จึงสามารถนำ dependency เหล่านี้ ที่ได้ดาวน์โหลดและคอมไพล์ไว้แล้วมาใช้ซ้ำได้
การรับประกันการคอมไพล์ซ้ำด้วยไฟล์ Cargo.lock
Cargo มีกลไกที่ช่วยให้คุณมั่นใจได้ว่า เมื่อคุณหรือใครก็ตามคอมไพล์โค้ดซ้ำอีกครั้งจะได้ผลลัพธ์เหมือนเดิม:
Cargo จะใช้เฉพาะเวอร์ชั่นของ dependency ที่คุณระบุไว้เท่านั้น จนกว่าคุณจะระบุเป็นอย่างอื่น
ตัวอย่างเช่น สมมติว่าสัปดาห์หน้าเวอร์ชั่น 0.8.6 ของ crate rand
ได้รับการเผยแพร่
และเวอร์ชั่นดังกล่าวมีการแก้ไขจุดบกพร่องที่สำคัญ แต่ยังมีข้อผิดพลาดซ้ำซ้อนที่จะทำให้โค้ดของคุณเสียหาย
เพื่อรับมือกับปัญหานี้ Rust จะสร้างไฟล์ Cargo.lock ในครั้งแรกที่คุณรัน cargo build
ดังนั้น ตอนนี้เรามีไฟล์นี้อยู่ในโฟลเดอร์ guessing_game แล้ว
เมือคุณคอมไพล์โปรเจกต์เป็นครั้งแรก Cargo จะค้นหาเวอร์ชั่นทั้งหมดของ dependency ที่ตรงตามเกณฑ์ จากนั้นเขียนทั้งหมดลงในไฟล์ Cargo.lock เมือคุณคอมไพล์โปรเจกต์ของคุณในอนาคต Cargo จะเห็นว่ามีไฟล์ Cargo.lock อยู่ และจะใช้เวอร์ชั่นที่ระบุไว้ในไฟล์นี้ แทนที่จะทำการค้นหาเวอร์ชั่นของ dependency ซ้ำอีกครั้ง วิธีนี้จะช่วยให้คุณคอมไพล์ซ้ำได้ได้โดยอัตโนมัติ กล่าวอีกนัยหนึ่ง โปรเจกต์ของคุณจะยังคงอยู่ในเวอร์ชั่น 0.8.5 จนกว่าคุณจะอัปเกรดโดยชัดเจน ขอบคุณไฟล์ Cargo.lock เนื่องจากไฟล์ Cargo.lock มีความสำคัญสำหรับการคอมไพล์ซ้ำ จึงมักถูกเพิ่มเข้าไปในระบบควบคุมเวอร์ชั่นพร้อมกับโค้ดส่วนอื่น ๆ ในโปรเจกต์ของคุณ
การอัปเดต crate เพื่อรับเวอร์ชั่นใหม่
เมื่อคุณต้องการอัปเดต crate Cargo ได้เตรียมคำสั่ง update
ซึ่งจะไม่สนใจไฟล์ Cargo.lock
และทำการค้นหาเวอร์ชั่นล่าสุดที่ตรงตามเงื่อนไขใน Cargo.toml
จากนั้น Cargo จะทำการเขียนเวอร์ชั่นเหล่านั้นลงบนไฟล์ Cargo.lock
ในกรณีนี้ Cargo จะมองหาเวอร์ชั่นที่สูงกว่า 0.8.5 และต่ำกว่า 0.9.0 เท่านั้น
หาก crate rand
มีการเผยแพร่สองเวอร์ชั่นคือ 0.8.6 และ 0.9.0
และคุณรันคำสั่ง cargo update
คุณจะเห็นผลลัพธ์ดังต่อไปนี้:
$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6
Cargo ไม่สนใจเวอร์ชั่น 0.9.0 ในจุดนี้ คุณจะสังเกตเห็นการเปลี่ยนแปลงในไฟล์ Cargo.lock ของคุณว่า
เวอร์ชั่นของ crate rand
ที่คุณกำลังใช้คือ 0.8.6
หากต้องการใช้ rand
เวอร์ชั่น 0.9.0 หรือเวอร์ชั่นใด ๆ ในชุด 0.9.x
คุณจะต้องแก้ไขไฟล์ Cargo.toml ให้เป็นดังนี้:
[dependencies]
rand = "0.9.0"
ครั้งถัดไปที่คุณรัน cargo build
Cargo จะอัปเดต registry ของ crate ที่มีอยู่
และประเมินความต้องการของ rand
ตามเวอร์ชั่นใหม่ที่คุณระบุ
มีเรื่องอื่นๆ มากมายที่จะพูดเกี่ยวกับ Cargo และ ecosystem ของมัน ซึ่งเราจะกล่าวถึงในบทที่ 14 แต่ในตอนนี้ นั่นคือทั้งหมดที่คุณจำเป็นต้องรู้ Cargo จะทำให้การนำไลบรารีกลับมาใช้ใหม่เป็นเรื่องที่ง่ายมาก ดังนั้น Rustaceans จึงสามารถเขียนโปรเจกต์ขนาดเล็กที่ประกอบขึ้นจากแพ็กเก็จต่าง ๆ ได้
การสร้างตัวเลขสุ่ม
มาเริ่มใช้ rand
ในการสุ่มเลขเพื่อทายกัน
ขั้นตอนถัดไปคือการเพิ่มโค้ดลงใน src/main.rs ดังที่แสดงในรายการ 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
จะประกาศ method ที่ตัวสร้างตัวเลขสุ่มนำไปใช้
และ trait นี้ต้องอยู่ในขอบเขตเดียวกัน เพื่อให้เราสามารถใช้ method เหล่านั้นได้ บทที่ 10 จะคลอบคลุมถึงรายละเอียดเกี่ยวกับ trait
ถัดไป เราจะเพิ่มสองบรรทัดตรงกลาง ในบรรทัดแรก เราจะเรียกใช้ฟังก์ชั่น rand::thread_rng
เพื่อสร้างตัวสุ่มเลขที่เราจะต้องใช้: ตัวสร้างเลขสุ่มนั้นจะอยู่ในเธรดปัจจุบันที่กำลังทำงาน และถูกกำหนดโดยระบบปฏิบัติการ
จากนั้นเราจะเรียก method gen_range
จากตัวสร้างเลขสุ่ม method นี้ถูกประกาศโดย trait Rng
ที่เราเพิ่มเข้ามาในขอบเขตด้วยบรรทัด use rand::Rng;
โดย gen_range
รับช่วงตัวเลขขอบเขตที่เป็นไปได้ด้วย argument
และสร้างตัวเลขสุ่มภายในช่วงขอบเขตนั้น ขอบเขตของค่าที่เราใช้นี้มีรูปแบบ start..=end
และรวมถึงขอบเขตล่างและบน ดังนั้นเราจึงจำเป็นต้องระบุ 1..=100
เพื่อขอจำนวนระหว่าง 1 ถึง 100
หมายเหตุ: คุณจะไม่เพียงรู้ว่าต้องใช้ trait ไหน method ไหน และฟังก์ชั่นไหนจาก crate ดังนั้นแต่ละ crate จะมีเอกสารคู่มือประกอบการใช้งาน คุณสมบัติเจ๋ง ๆ อีกอย่างหนึ่งของ Cargo คือการรันคำสั่ง
cargo doc --open
ซึ่งจะสร้างเอกสารคู่มือจากทุก dependency ของคุณบนเครื่อง และเปิดเอกสารบนเบราว์เซอร์ของคุณ ตัวอย่างเช่น หากคุณสนใจฟังก์ชั่นอื่น ๆ ใน craterand
รันคำสั่งcargo doc --open
และคลิกrand
ในแถบด้านซ้าย
บรรทัดใหม่ลำดับที่สองจะแสดงตัวเลขลับจากการสุ่ม ซึ่งมีประโยชน์ในขณะที่เรากำลังพัฒนาโปรแกรมเพื่อทดสอบ แต่เราจะลบมันออกในเวอร์ชั่นสุดท้าย คงจะไม่ใช่เกมสักเท่าไหร่หากโปรแกรมแสดงคำตอบทันทีที่เริ่มทำงาน
ลองรันโปรแกรมสักสองสามครั้ง:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
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 [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 ของผู้ใช้และตัวเลขสุ่มแล้ว เราสามารถเปรียบเทียบมันได้ ขั้นตอนดังกล่าวแสดงอยู่ในรายการ 2-4 โปรดทราบว่าโค้ดนี้จะยังไม่คอมไพล์ตามที่เราได้อธิบาย
use rand::Rng;
use std::cmp::Ordering;
use std::io;
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!"),
}
}
ขั้นแรก เราเพิ่มคำสั่ง use
อีกบรรทัด โดยนำเข้าประเภทตัวแปรที่เรียกว่า std::cmp::Ordering
เข้ามายังขอบเขตจากไลบรารีมาตรฐาน ประเภทตัวแปร Ordering
เป็น enum อีกหนึ่งแบบ
และมี variant คือ Less
, Greater
, และ Equal
ซึ่งก็คือผลลัพธ์สามประการที่เป็นไปได้
เมื่องคุณเปรียบเทียบค่าสองค่า
จากนั้นเราจะเพิ่มบรรทัดใหม่ห้าบรรทัดที่ด้านล่าง ซึ่งใช้งานประเภทตัวแปร Ordering
ที่มี method cmp
ที่สามารถเปรียบเทียบค่าสองค่า และสามารถเรียกใช้กับอะไรก็ได้ที่สามารถเปรียบเทียบได้
โดยมันจะอ้างถึงสิ่งที่คุณเปรียบเทียบ: ในที่นี้จะเป็นการเปรียบเทียบ guess
กับ secret_number
จากนั้น return ค่า variant ของ enum Ordering
ที่เราได้นำเข้ามายังขอบเขตผ่านคำสั่ง use
จากนั้นเราใช้ match
expression เพื่อตัดสินใจว่าจะทำอย่างไรต่อไป
โดยขึ้นอยู่กับค่า variant ของ Ordering
ที่ถูก return จากการเรียกใช้ cmp
กับค่าใน guess
และ secret_number
match
expression ประกอบด้วยแขน และแขนประกอบด้วยรูปแบบที่จะจับคู่กัน
และโค้ดที่ควรจะถูกรันเมื่อค่าที่กำหนดตรงกับรูปแบบของแขน Rust จะนำค่าที่กำหนดให้กับ match
และตรวจสอบรูปแบบของแขนตามลำดับ รูปแบบและโครงสร้าง match
เป็นคุณสมบัติที่ทรงพลัง:
ซึ่งช่วยให้คุณสามารถแสดงสถานการณ์ต่าง ๆ ที่โค้ดของคุณอาจพบเจอ และทำให้มั่นใจว่าคุณจะจัดการกับเหตุการณ์เหล่านั้นได้
คุณสมบัติเหล่านี้จะถูกกล่าวถึงโดยละเอียดในบทที่ 6 และบทที่ 19 ตามลำดับ
มาดูตัวอย่างด้วย match
expression ที่เราใช้ที่นี่
สมมติว่าผู้ใช้ทายหมายเลข 50 และหมายเลขที่สร้างขึ้นแบบสุ่มในครั้งนี้คือ 38
เมื่อโค้ดทำการเปรียบเทียบ 50 กับ 38 method cmp
จะ return
ค่า Ordering::Greater
เพราะ 50 นั้นมากกว่า 38 match
expression จะได้รับค่า
Ordering::Greater
และเริ่มต้นตรวจสอบรูปแบบของแต่ละแขน
โดยจะเริ่มดูที่แขนแรก Ordering::Less
และตรวจสอบว่าค่า Ordering::Greater
นั้นไม่ตรงกับ Ordering::Less
ดังนั้นจึงไม่สนใจโค้ดในแขนนั้น และย้ายไปที่แขนถัดไป
รูปแบบของแขนลำดับถัดไปคือ Ordering::Greater
ซึ่งตรงกับ Ordering::Greater
!
ดังนั้นโค้ดที่เกี่ยวข้องในแขนนี้จะทำงาน และแสดงผล Too big!
ไปยังหน้าจอ
match
expression จบการทำงานหลังจากรูปแบบตรงกันในครั้งแรก
ดังนั้นมันจะไม่ตรวจสอบแขนสุดท้ายในสถานการณ์นี้
อย่างไรก็ตาม โค้ดในรายการที่ 2-4 จะยังไม่สามารถคอมไพล์ มาลองดูกัน:
$ cargo build
Downloading crates ...
Downloaded rand_core v0.6.2
Downloaded getrandom v0.2.2
Downloaded rand_chacha v0.3.0
Downloaded ppv-lite86 v0.2.10
Downloaded libc v0.2.86
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:22:21
|
22 | 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/eeb90cda1969383f56a2637cbd3037bdf598841c/library/core/src/cmp.rs:839: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
ใจความโดยสรุปของข้อผิดพลาดคือ มีประเภทตัวแปรที่ไม่ตรงกัน
Rust มีระบบประเภทตัวแปรคงที่ที่แข็งแกร่ง อย่างไรก็ตาม มันก็ยังมีการอนุมานประเภทตัวแปร
เมื่อเราเขียน let mut guess = String::new()
Rust สามารถอนุมานได้ว่า guess
นั้นควรเป็น String
โดยไม่จำเป็นต้องระบุประเภท
ในทางกลับกัน secret_number
คือประเภทตัวเลข
ประเภทตัวแปรของ Rust บางส่วนที่สามารถมีค่าระหว่าง 1 ถึง 100: i32
คือตัวเลข 32 บิต;
u32
คือตัวเลขแบบ unsigned 32 บิต; i64
คือตัวเลข 64 บิต; และอื่น ๆ
โดยหากไม่ได้ระบุไว้ว่าเป็นประเภทใด Rust จะกำหนดโดยค่าเริ่มต้นเป็น i32
ซึ่งก็คือประเภทของ secret_number
เว้นแต่ว่าจะมีข้อมูลเกี่ยวกับประเภทตัวแปรที่ทำให้ Rust อนุมานเป็นประเภทตัวแปรอื่น
สาเหตุของข้อผิดพลาดคือ Rust ไม่สามารถเปรียบเทียบประเภท string กับตัวเลขได้
ท้ายสุด เราต้องการแปลง String
ที่โปรแกรมอ่านเป็น input ไปเป็นประเภทตัวเลข
เพื่อให้เราสามารถนำมันไปเปรียบเทียบกับตัวเลขสุ่มได้
โดยเราจะเพิ่มบรรทัดนี้ไปที่ภายในฟังก์ชั่น main
:
ชื่อไฟล์: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
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 ที่มีประโยชน์ช่วยให้เราสามารถบดบังค่าเดิมของ guess
ด้วยค่าใหม่ได้
การบดบังช่วยให้เราสามารถนำชื่อตัวแปร guess
มาใช้ซ้ำได้ แทนที่จะต้องสร้างตัวแปรสองตัวที่ไม่ซ้ำกัน
เช่น guess_str
และ guess
เราจะกล่าวถึงเรื่องนี้โดยละเอียดใน บทที่ 3
แต่ในตอนนี้ รู้ไว้ว่าคุณสมบัตินี้มักถูกใช้เมื่อคุณต้องการแปลงค่าจากชนิดหนึ่งไปยังอีกชนิดหนึ่ง
เรากำหนดค่าตัวแปรใหม่ด้วย guess.trim().parse()
โดย guess
ในโค้ดนี้อ้างถึงตัวแปร guess
ดั้งเดิมที่มี input เป็น string
ส่วน method trim
บนอินสแตนซ์ String
จะสบช่องว่างที่จุดเริ่มต้นและจุดสิ้นสุด
ซึ่งเราต้องทำเพื่อให้สามารถเปรียบเทียบ string กับ u32
ที่สามารถมีได้เฉพาะข้อมูลตัวเลขเท่านั้น
ผู้ใช้ต้องกด enter เพื่อให้เป็นไปตาม read_line
และป้อนการคาดเดา
ซึ่งมีผลทำให้มีตัวอักขระบรรทัดใหม่เพิ่มไปยัง string ตัวอย่างเช่น
หากผู้ใช้พิมพ์ 5 และกด enter guess
จะเป็นดังนี้:
5\n
โดย \n
นั้นหมายถึง “บรรทัดใหม่” (บน Windows การกด enter จะส่งผลให้เกิด carriage return
และบรรทัดใหม่, \r\n
) method trim
จะลบ \n
หรือ \r\n
ส่งผลให้เหลือเพียง 5
method parse
บน strings จะทำการแปลง string เป็นประเภทอื่น
ในที่นี้เราจะใช้มันเพื่อแปลงจาก string ไปเป็นตัวเลข เราจำเป็นต้องบอกประเภทตัวเลขที่ชัดเจนกับ Rust
โดยใช้ let guess: u32
เครื่องหมายทวิภาค (:
) ถัดจาก guess
บอก Rust
ว่าเราจะกำหนดประเภทตัวแปรใด Rust มีประเภทตัวเลขในตัวเล็กน้อย ซึ่ง u32
ที่ได้เห็นไปนั้นคือ
ตัวเลข unsigned 32 บิต มันเป็นตัวเลือกเริ่มต้นที่ดีสำหรับจำนวนเต็มบวกขนาดเล็ก
คุณจะได้เรียนรู้เกี่ยวกับประเภทของตัวเลขอื่น ๆ ใน บทที่ 3
นอกจากนี้ การระบุ u32
ในโปรแกรมตัวอย่างและการเปรียบเทียบกับ secret_number
ยังหมายถึง Rust จะอนุมานว่า secret_number
ควรจะเป็น u32
เช่นกัน
ตอนนี้การเปรียบเทียบอยู่ระหว่างสองค่าของประเภทตัวแปรเดียวกัน!
method parse
จะใช้ได้เฉพาะกับอักขระที่สามารถแปลงเป็นตัวเลขได้ตามตรรกะเท่านั้น
และอาจทำให้เกิดข้อผิดพลาดได้ง่าย ตัวอย่างเช่น หาก string มี A👍%
รวมอยู่ด้วย
มันจะไม่มีทางแปลงเป็นตัวเลข เนื่องจากอาจล้มเหลว method parse
จึง return ประเภท Result
คล้ายกับที่ read_line
ทำ (อธิบายไว้ก่อนหน้านี้ใน
“การจัดการกับความล้มเหลวที่อาจจะเกิดขึ้นด้วย Result”)
เราจะปฏิบัติต่อ Result
นี้ในลักษณะเดียวกันโดยใช้ method expect
อีกครั้ง
หาก parse
ทำการ return Err
ซึ่งเป็น variant ของ
Result
เนื่องจากไม่สามารถสร้างตัวเลขจาก string ได้ expect
จะทำให้เกมหยุดทำงานและแสดงข้อความที่กำหนด
หาก parse
สามารถแปลง string เป็นตัวเลขได้สำเร็จ มันจะ return Ok
ซึ่งเป็น variant ของ Result
และ expect
จะ return ตัวเลขที่เราต้องการจากค่า Ok
ตอนนี้มารันโปรแกรมกัน:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
ทำได้ดี! แม้ว่าจะมีการเติมช่องว่างข้างหน้า guess แต่โปรแกรมก็ยังรู้ได้ว่าผู้ใช้ทาย 76 รันโปรแกรมสองสามครั้งเพื่อตรวจสอบพฤติกรรมที่แตกต่างกันด้วย input ประเภทต่าง ๆ : เดาตัวเลขที่ถูกต้อง เดาตัวเลขที่สูงเกินไป และเดาตัวเลขที่ต่ำเกินไป
ขณะนี้ส่วนใหญ่ของเกมทำงานได้แล้ว แต่ผู้ใช้สามารถทายได้เพียงครั้งเดียว มาแก้ไขด้วยการเพิ่มการวนซ้ำกัน!
อนุญาตให้ทายได้หลายครั้งด้วยการวนซ้ำ
คำสั่ง loop
จร้างสร้างการวนซ้ำที่ไม่สิ้นสุด เราจะเพิ่มการวนซ้ำเพื่อให้ผู้ใช้มีโอกาสเดาหมายเลขมากขึ้น:
ชื่อไฟล์: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
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!"),
}
}
}
อย่างที่คุณ เราได้ย้ายทุกอย่างตั้งแต่ข้อความแจ้งให้ผู้ใช้ป้อน input เป็นต้นไปเข้าไปยัง loop ตรวจสอบให้แน่ใจว่าได้เยื้องบรรทัดภายใน loop อีกสี่ช่องว่างในแต่ละบรรทัดแล้วรันโปรแกรมอีกครั้ง ตอนนี้โปรแกรมจะขอให้คุณทายอย่างไม่มีที่สิ้นสุด ซึ่งจริง ๆ แล้วทำให้เกิดปัญหาใหม่ ดูเหมือนว่าผู้ใช้ไม่สามารถออกจากโปรแกรมได้!
ผู้ใช้สามารถขัดจังหวะโปรแกรมได้ตลอดเวลา โดยใช้คีย์ลัด ctrl-c
แต่มีอีกวิธีในการหลบหนีสัตว์ประหลาดที่ไม่รู้จักพอนี้ ดังที่กล่าวถึงในการอธิบาย parse
ใน
“การเปรียบเทียบตัวเลขทายและตัวเลขที่ถูกต้อง”:
หากผู้ใช้ป้อนคำตอบที่ไม่ใช่ตัวเลข โปรแกรมจะหยุดการทำงาน เราสามารถใช้ประโยชน์จากสิ่งนี้เพื่อให้ผู้ใช้สามารถออกจากโปรแกรมได้
ดังต่อไปนี้:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
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 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
การพิมพ์ quit
เป็นการออกจากเกม แต่อย่างที่คุณสังเกตเห็น การป้อนข้อมูลอื่น ๆ ที่ไม่ใช่ตัวเลขก็จะเป็นเช่นเดียวกัน
นี่เป็นสิ่งที่ไม่ดีนัก; เราต้องการให้เกมหยุดเมื่อทายตัวเลขถูกต้อง
ออกจากโปรแกรมเมื่อทายถูกต้อง
มาเขียนโปรแกรมให้ออกจากเกมเมื่อผู้ใช้ชนะ โดยเพิ่ม break
ลงในโค้ดกัน:
ชื่อไฟล์: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
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 ที่ไม่ถูกต้อง
เพื่อปรับแต่งพฤติกรรมของเกมเพิ่มเติม แทนที่จะทำให้โปรแกรมหยุดทำงานเมื่อผู้ใช้ป้อน input ที่ไม่ใช่ตัวเลข
มาทำให้เกมละเว้น input ที่ไม่ใช่ตัวเลขเพื่อให้ผู้ใช้สามารถทายตัวเลขต่อไปได้
เราสามารถทำได้โดยการเปลี่ยนบรรทัดที่ guess
ถูกแปลงจาก String
ไปเป็น u32
ดังที่แสดงในรายการที่ 2-5
use rand::Rng;
use std::cmp::Ordering;
use std::io;
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 เพื่อย้ายจากการทำให้โปรแกรมหยุดทำงานจากข้อผิดพลาด
มาเป็นการจัดการกับข้อผิดพลาด โปรดจำไว้ว่า parse
นั้นจะ return ค่าประเภท Result
ซึ่งเป็น enum
ที่มี variant คือ Ok
และ Err
เราสามารถใช้ match
expression ตรงนี้ เช่นเดียวกับที่เราทำกับ
ผลลัพธ์ Ordering
จาก method cmp
หาก parse
สามารถแปลง string ไปเป็นตัวเลขได้สำเร็จ มันจะ return ค่าเป็น Ok
ซึ่งจะมีตัวเลขผลลัพธ์อยู่ในนั้นด้วย ค่า Ok
จะตรงกับรูปแบบของแขนลำดับแรก และ match
expression
จะแค่ return ค่า num
ที่ parse
นั้นสร้างขึ้นมาและนำไปใส่ไว้ในค่า Ok
จากนั้นตัวเลขนี้จะถูกกำหนดค่าลงในตัวแปร guess
ตัวใหม่ที่เราได้สร้างขึ้น
หาก parse
ไม่สามารถแปลง string ไปเป็นตัวเลขได้ มันจะ return ค่า Err
ซึ่งจะมีข้อมูลเกี่ยวกับ
error รวมอยู่ในนั้นด้วย ค่า Err
ไม่ตรงกับรูปแบบ Ok(num)
ในแขนแรกของ match
แต่มันตรงกับรูปแบบ Err(_)
ในแขนที่สอง ขีดเล้นใต้ _
เป็นค่าที่รับทั้งหมด ในตัวอย่างนี้
เรากำลังบอกว่าเราต้องการจับคู่ค่า Err
ทั้งหมด ไม่ว่าค่าเหล่านั้นจะมีข้อมูลอะไรอยู่ภายในก็ตาม
ดังนั้นโปรแกรมจะรันโค้ดในแขนที่สอง continue
นี้บอกว่าให้โปรแกรมไปที่การวนซ้ำครั้งถัดไปของ loop
และขอให้เดาอีกครั้ง
ดังนั้นโปรแกรมจะไม่สนใจข้อผิดพลาดทั้งหมดที่อาจพบใน parse
อย่างมีประสิทธิภาพ!
ตอนนี้ทุกอย่างในโปรแกรมควรจะทำงานได้ตามที่คาดไว้ มาลองดูกัน:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 4.45s
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 rand::Rng;
use std::cmp::Ordering;
use std::io;
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;
}
}
}
}
ถึงจุดนี้ คุณได้สร้างเกมทายตัวเลขสำเร็จแล้ว ยินดีด้วย!
สรุป
โปรเจกต์นี้เป็นการลงมือปฏิบัติจริงเพื่อแนะนำให้คุณรู้จักกับแนวคิดใหม่ ๆ ของ Rust มากมาย:
let
, match
, ฟังก์ชั่น, การใช้ crate ภายนอก และอื่น ๆ อีกมากมาย
ในอีกไม่กี่บทต่อจากนี้คุณจะได้เรียนรู้เกี่ยวกับแนวคิดเหล่านี้โดยละเอียดมากขึ้น บทที่ 3 ครอบคลุมแนวคิดที่ภาษาโปรแกรมส่วนใหญ่มี
เช่น ตัวแปร ชนิดข้อมูล และฟังก์ชั่น และแสดงวิธีการใช้งานใน Rust บทที่ 4 สำรวจเกี่ยวกับ ownership
ซึ่งเป็นคุณสมบัติที่ทำให้ Rust แตกต่างจากภาษาโปรแกรมอื่น บทที่ 5 กล่าวถึง struct และไวยากรณ์ method
และบทที่ 6 สำรวจเกี่ยวกับการทำงานของ enum