Programming a Guessing Game
Hãy bắt đầu với Rust bằng cách làm một dự án thực hành cùng nhau! Chương này giới thiệu một vài khái niệm phổ biến trong Rust bằng cách cho bạn thấy cách sử dụng chúng trong một chương trình thực tế. Bạn sẽ học về let
, match
, methods, associated functions, external crates, và nhiều hơn nữa! Trong các chương tiếp theo, chúng ta sẽ khám phá những ý tưởng này chi tiết hơn. Ở chương này, bạn sẽ chỉ thực hành các kiến thức cơ bản.
Chúng ta sẽ triển khai một bài toán cổ điển cho người mới bắt đầu: trò chơi đoán số. Cách chơi như sau: chương trình sẽ sinh một số nguyên ngẫu nhiên từ 1 đến 100. Sau đó nó sẽ yêu cầu người chơi nhập một dự đoán. Sau khi nhập một dự đoán, chương trình sẽ cho biết dự đoán đó quá thấp hay quá cao. Nếu dự đoán đúng, trò chơi sẽ in thông báo chúc mừng và thoát.
Setting Up a New Project
Để thiết lập một dự án mới, vào thư mục projects mà bạn đã tạo ở Chương 1 và tạo một dự án mới bằng Cargo, như sau:
$ cargo new guessing_game
$ cd guessing_game
Lệnh đầu tiên, cargo new
, nhận tên dự án (guessing_game
) làm đối số đầu tiên. Lệnh thứ hai chuyển đến thư mục dự án mới.
Hãy xem file Cargo.toml được sinh ra:
Filename: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[dependencies]
Như bạn đã thấy ở Chương 1, cargo new
tạo một chương trình “Hello, world!” cho bạn. Hãy xem file src/main.rs:
Filename: src/main.rs
fn main() { println!("Hello, world!"); }
Bây giờ hãy biên dịch chương trình “Hello, world!” này và chạy nó trong cùng một bước bằng lệnh 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!
Lệnh run
rất hữu ích khi bạn cần lặp nhanh trên một dự án, như chúng ta sẽ làm trong trò chơi này, kiểm tra từng phiên bản trước khi chuyển sang bước tiếp theo.
Mở lại file src/main.rs. Bạn sẽ viết tất cả mã trong file này.
Processing a Guess
Phần đầu của chương trình đoán số sẽ hỏi đầu vào từ người dùng, xử lý đầu vào đó, và kiểm tra rằng đầu vào có đúng định dạng mong đợi. Để bắt đầu, chúng ta sẽ cho phép người chơi nhập một dự đoán. Nhập mã ở Listing 2-1 vào 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}");
}
Mã này chứa nhiều thông tin, vì vậy hãy cùng đi qua từng dòng. Để lấy đầu vào từ người dùng rồi in kết quả ra, chúng ta cần đưa thư viện io
input/output vào phạm vi. Thư viện io
nằm trong standard library, gọi là 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}");
}
Theo mặc định, Rust có một tập hợp các mục được định nghĩa trong standard library mà nó đưa vào phạm vi của mọi chương trình. Tập hợp này gọi là prelude, và bạn có thể xem tất cả trong đó trong tài liệu standard library.
Nếu một kiểu bạn muốn dùng không nằm trong prelude, bạn phải đưa kiểu đó vào phạm vi rõ ràng bằng một câu lệnh use
. Việc sử dụng thư viện std::io
cung cấp cho bạn nhiều tính năng hữu ích, bao gồm khả năng chấp nhận đầu vào từ người dùng.
Như bạn đã thấy ở Chương 1, hàm main
là điểm vào chương trình:
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}");
}
Cú pháp fn
khai báo một hàm mới; các dấu ngoặc đơn, ()
, biểu thị không có tham số; và dấu ngoặc nhọn, {
, bắt đầu thân hàm.
Như bạn cũng đã học ở Chương 1, println!
là một macro in một chuỗi ra màn hình:
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}");
}
Mã này in một dòng nhắc nói trò chơi là gì và yêu cầu nhập.
Storing Values with Variables
Tiếp theo, chúng ta sẽ tạo một biến để lưu đầu vào của người dùng, như sau:
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}");
}
Bây giờ chương trình trở nên thú vị! Có rất nhiều thứ diễn ra trong dòng nhỏ này. Chúng ta dùng câu lệnh let
để tạo biến. Đây là một ví dụ khác:
let apples = 5;
Dòng này tạo một biến mới tên là apples
và gán giá trị 5 cho nó. Trong Rust, biến mặc định là immutable, nghĩa là khi đã gán giá trị cho biến, giá trị đó sẽ không thay đổi. Chúng ta sẽ bàn kỹ khái niệm này trong “Variables and Mutability” ở Chương 3. Để biến thành mutable, ta thêm mut
trước tên biến:
let apples = 5; // immutable
let mut bananas = 5; // mutable
Note: Cú pháp //
bắt đầu một chú thích tiếp tục đến cuối dòng. Rust bỏ qua mọi thứ trong chú thích. Chúng ta sẽ thảo luận chi tiết hơn về chú thích trong Chương 3.
Quay lại chương trình đoán số, giờ bạn biết let mut guess
sẽ khai báo một biến mutable tên guess
. Dấu bằng (=
) báo cho Rust biết chúng ta muốn gán một cái gì đó cho biến. Ở bên phải dấu bằng là giá trị được gán cho guess
, là kết quả của việc gọi String::new
, một hàm trả về một thể hiện mới của String
. String
là một kiểu chuỗi do standard library cung cấp, có khả năng mở rộng, mã hóa UTF-8.
Cú pháp ::
trong String::new
biểu thị rằng new
là một associated function của kiểu String
. Một associated function là một hàm được triển khai trên một kiểu; ở đây new
tạo một chuỗi rỗng mới. Bạn sẽ thấy hàm new
trên nhiều kiểu vì đó là tên phổ biến cho hàm tạo giá trị mới.
Toàn bộ dòng let mut guess = String::new();
đã tạo một biến mutable hiện được gán một thể hiện String
mới và rỗng. Whew!
Receiving User Input
Nhớ rằng chúng ta đã đưa chức năng input/output từ standard library vào phạm vi với use std::io;
ở đầu chương trình. Bây giờ chúng ta sẽ gọi hàm stdin
từ module io
, cho phép xử lý đầu vào từ người dùng:
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}");
}
Nếu chúng ta chưa import module io
với use std::io;
ở đầu chương trình, chúng ta vẫn có thể dùng hàm này bằng cách viết std::io::stdin
. Hàm stdin
trả về một thể hiện của std::io::Stdin
, là kiểu đại diện cho một handle tới input chuẩn của terminal.
Tiếp theo, dòng .read_line(&mut guess)
gọi method read_line
trên handle input chuẩn để lấy đầu vào từ người dùng. Chúng ta cũng truyền &mut guess
làm đối số cho read_line
để cho nó biết chuỗi nào sẽ lưu đầu vào người dùng. Công việc đầy đủ của read_line
là lấy những gì người dùng gõ vào standard input và nối vào chuỗi (không ghi đè nội dung hiện có), nên chúng ta truyền chuỗi đó như một đối số. Đối số chuỗi phải là mutable để method có thể thay đổi nội dung chuỗi.
Dấu &
chỉ ra rằng đối số này là một reference, cho phép nhiều phần mã truy cập cùng một dữ liệu mà không cần sao chép nhiều lần. References là tính năng phức tạp, và một trong những lợi thế lớn của Rust là cách dùng references an toàn và dễ dàng. Bạn không cần hiểu nhiều chi tiết để hoàn thành chương trình này. Hiện tại, bạn chỉ cần biết references mặc định là immutable, vì vậy bạn phải viết &mut guess
thay vì &guess
để làm nó mutable. (Chương 4 sẽ giải thích references kỹ hơn.)
Handling Potential Failure with Result
Chúng ta vẫn đang thảo luận dòng mã này. Chúng ta đang nói về phần thứ ba của dòng lệnh hợp lệ về mặt logic. Phần tiếp theo là method này:
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}");
}
Chúng ta có thể viết dòng này như sau:
io::stdin().read_line(&mut guess).expect("Failed to read line");
Tuy nhiên, một dòng dài khó đọc, nên tốt hơn là chia nhỏ. Thường thì nên chèn dấu xuống dòng và khoảng trắng khác để giúp tách các dòng dài khi gọi method theo cú pháp .method_name()
. Bây giờ hãy bàn về dòng này.
Như đã đề cập, read_line
đặt những gì người dùng nhập vào chuỗi mà chúng ta truyền, nhưng nó cũng trả về một giá trị Result
. Result
là một enumeration, thường gọi là enum, là một kiểu có thể ở một trong nhiều trạng thái. Chúng ta gọi mỗi trạng thái có thể là một variant.
Chương 6 sẽ trình bày enum chi tiết hơn. Mục đích của các kiểu Result
là để mã hóa thông tin xử lý lỗi.
Các biến thể của Result
là Ok
và Err
. Variant Ok
chỉ ra rằng thao tác thành công, và nó chứa giá trị tạo ra. Variant Err
nghĩa là thao tác thất bại và chứa thông tin về lý do thất bại.
Các giá trị kiểu Result
, giống như mọi kiểu khác, có các phương thức được định nghĩa trên chúng. Một thể hiện của Result
có method expect
mà bạn có thể gọi. Nếu thể hiện này là Err
, expect
sẽ khiến chương trình dừng và in thông báo mà bạn truyền làm đối số. Nếu read_line
trả về Err
, nó có thể là lỗi từ hệ điều hành. Nếu thể hiện Result
là Ok
, expect
sẽ lấy giá trị trong Ok
và trả về giá trị đó để bạn sử dụng. Trong trường hợp này, giá trị đó là số byte trong đầu vào của người dùng.
Nếu bạn không gọi expect
, chương trình vẫn biên dịch nhưng bạn sẽ nhận được một cảnh báo:
$ 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 cảnh báo rằng bạn chưa sử dụng giá trị Result
trả về từ read_line
, ám chỉ rằng chương trình chưa xử lý lỗi có thể xảy ra.
Cách đúng để loại bỏ cảnh báo là thực sự viết mã xử lý lỗi, nhưng trong trường hợp này chúng ta chỉ muốn chương trình dừng khi xảy ra vấn đề, nên có thể dùng expect
. Bạn sẽ học cách phục hồi từ lỗi trong Chương 9.
Printing Values with println!
Placeholders
Ngoài dấu ngoặc nhọn đóng, chỉ còn một dòng nữa để bàn trong mã tới lúc này:
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}");
}
Dòng này in chuỗi giờ chứa đầu vào của người dùng. {}
là một placeholder: hãy nghĩ {}
như đôi càng nhỏ giữ một giá trị. Khi in giá trị của một biến, tên biến có thể đặt bên trong cặp ngoặc nhọn. Khi in kết quả của một biểu thức, đặt cặp ngoặc nhọn rỗng trong chuỗi định dạng, rồi theo sau chuỗi định dạng bằng danh sách các biểu thức phân tách bằng dấu phẩy sẽ in vào từng placeholder theo thứ tự tương ứng. Việc in một biến và kết quả của một biểu thức trong một lần gọi println!
trông như sau:
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {x} and y + 2 = {}", y + 2); }
Mã này sẽ in x = 5 and y + 2 = 12
.
Testing the First Part
Hãy thử phần đầu tiên của trò chơi. Chạy bằng 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
Tại thời điểm này, phần đầu của trò chơi đã xong: chúng ta đang nhận dữ liệu từ bàn phím rồi in ra.
Generating a Secret Number
Tiếp theo, chúng ta cần sinh một số bí mật mà người dùng sẽ dự đoán. Số bí mật nên khác nhau mỗi lần để trò chơi thú vị khi chơi nhiều lần. Chúng ta sẽ dùng một số ngẫu nhiên từ 1 đến 100 để trò chơi không quá khó. Rust chưa có sẵn chức năng số ngẫu nhiên trong standard library. Tuy nhiên, đội ngũ Rust cung cấp crate rand
với chức năng đó.
Using a Crate to Get More Functionality
Hãy nhớ rằng crate là một tập hợp các file mã nguồn Rust. Dự án chúng ta đang xây là một binary crate, tức là một executable. Crate rand
là một library crate, chứa mã được dùng trong chương trình khác và không thể chạy độc lập.
Sự quản lý các crate ngoài rất hiệu quả của Cargo là điểm mạnh của nó. Trước khi viết mã dùng rand
, chúng ta cần sửa file Cargo.toml để thêm crate rand
làm dependency. Mở file đó và thêm dòng sau vào cuối, dưới header [dependencies]
mà Cargo đã tạo cho bạn. Hãy chắc chắn ghi rand
chính xác như ở đây, với số phiên bản này, nếu không các ví dụ trong tutorial có thể không hoạt động:
Filename: Cargo.toml
[dependencies]
rand = "0.8.5"
Trong file Cargo.toml, mọi thứ theo sau một header là phần thuộc về section đó cho tới khi một section khác bắt đầu. Trong [dependencies]
bạn nói Cargo biết dự án phụ thuộc những crate ngoài nào và phiên bản nào của chúng. Ở đây, chúng ta chỉ định crate rand
với semantic version 0.8.5
. Cargo hiểu Semantic Versioning (SemVer), một chuẩn cho việc viết số phiên bản. Specifier 0.8.5
thực ra là viết tắt của ^0.8.5
, nghĩa là bất kỳ phiên bản nào ít nhất là 0.8.5 nhưng nhỏ hơn 0.9.0.
Cargo coi những phiên bản này có API công khai tương thích với 0.8.5, và chỉ định này đảm bảo bạn sẽ lấy bản vá mới nhất mà vẫn biên dịch được với mã trong chương này. Mọi phiên bản 0.9.0 trở lên không đảm bảo có cùng API với ví dụ sau đây.
Bây giờ, không thay đổi mã, hãy build dự án, như trong 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
after adding the rand crate as a dependencyBạn có thể thấy các số phiên bản khác nhau (nhưng tất cả sẽ tương thích với mã, nhờ SemVer!) và các dòng khác nhau (tùy hệ điều hành), và thứ tự các dòng có thể khác.
Khi chúng ta thêm một dependency ngoài, Cargo tải các phiên bản mới nhất của mọi thứ dependency đó cần từ registry, tức là bản sao dữ liệu từ Crates.io. Crates.io là nơi mọi người đăng dự án mã nguồn mở Rust để người khác dùng.
Sau khi cập nhật registry, Cargo kiểm tra section [dependencies]
và tải về bất kỳ crate nào được liệt kê mà chưa có. Trong trường hợp này, mặc dù chỉ liệt kê rand
, Cargo cũng tải về các crate khác mà rand
phụ thuộc. Sau khi tải về, Rust biên dịch chúng rồi biên dịch dự án với các dependency có sẵn.
Nếu bạn chạy cargo build
ngay lập tức lần nữa mà không thay đổi gì, bạn sẽ không thấy gì ngoài dòng Finished
. Cargo biết nó đã tải và biên dịch dependencies, và bạn không thay đổi Cargo.toml. Cargo cũng biết bạn chưa thay đổi mã, nên không biên dịch lại. Không có gì để làm, nó thoát.
Nếu bạn mở src/main.rs, thay đổi một chút, lưu và build lại, bạn chỉ sẽ thấy hai dòng:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
Những dòng này cho thấy Cargo chỉ cập nhật build với thay đổi nhỏ trên src/main.rs. Dependencies của bạn không thay đổi, nên Cargo biết có thể tái sử dụng những gì đã tải và biên dịch.
Ensuring Reproducible Builds with the Cargo.lock File
Cargo có cơ chế đảm bảo bạn có thể rebuild cùng một artifact mỗi lần: Cargo sẽ chỉ dùng các phiên bản dependencies bạn chỉ định cho tới khi bạn nói khác. Ví dụ, giả sử tuần tới phiên bản 0.8.6 của crate rand
ra mắt, và phiên bản đó có bản sửa lỗi quan trọng nhưng cũng mang theo một regression làm hỏng mã của bạn. Để xử lý, Rust tạo file Cargo.lock lần đầu bạn chạy cargo build
, vì vậy giờ chúng ta có file này trong thư mục guessing_game.
Khi build dự án lần đầu, Cargo tìm tất cả các phiên bản dependencies phù hợp và ghi chúng vào Cargo.lock. Khi build trong tương lai, Cargo sẽ thấy file Cargo.lock tồn tại và dùng các phiên bản được liệt kê đó thay vì suy luận lại. Điều này cho phép bạn có một build có thể tái tạo. Nói cách khác, dự án của bạn sẽ vẫn dùng 0.8.5 trừ khi bạn nâng cấp rõ ràng, nhờ Cargo.lock. Vì Cargo.lock quan trọng cho build có thể tái tạo, nó thường được commit vào source control cùng mã dự án.
Updating a Crate to Get a New Version
Khi bạn muốn cập nhật crate, Cargo cung cấp lệnh update
, lệnh này sẽ bỏ qua Cargo.lock và tìm các phiên bản mới nhất phù hợp với chỉ định trong Cargo.toml. Cargo rồi sẽ ghi các phiên bản đó vào Cargo.lock. Trong trường hợp này, Cargo chỉ tìm các phiên bản > 0.8.5 và < 0.9.0. Nếu crate rand
phát hành hai phiên bản mới 0.8.6 và 0.9.0, bạn sẽ thấy như sau nếu chạy 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.9.0)
Cargo bỏ qua bản 0.9.0. Lúc này bạn cũng sẽ thấy thay đổi trong Cargo.lock ghi rằng phiên bản rand
đang dùng là 0.8.6. Để dùng rand
phiên bản 0.9.0 hoặc bất kỳ phiên bản nào trong series 0.9.x, bạn phải cập nhật Cargo.toml như sau:
[dependencies]
rand = "0.9.0"
Lần tiếp theo bạn chạy cargo build
, Cargo sẽ cập nhật registry và đánh giá lại yêu cầu rand
theo phiên bản mới bạn chỉ định.
Còn nhiều điều để nói về Cargo và hệ sinh thái của nó, mà chúng ta sẽ thảo luận ở Chương 14, nhưng hiện tại chỉ cần biết đến đây. Cargo làm cho việc tái sử dụng thư viện trở nên dễ dàng, nên cộng đồng Rust viết các dự án nhỏ lắp ghép từ nhiều package.
Generating a Random Number
Hãy bắt đầu dùng rand
để sinh số cần đoán. Bước tiếp theo là cập nhật src/main.rs, như trong 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}");
}
Đầu tiên chúng ta thêm dòng use rand::Rng;
. Trait Rng
định nghĩa các method mà các random number generators triển khai, và trait này phải nằm trong phạm vi để ta có thể dùng các method đó. Chương 10 sẽ trình bày trait chi tiết.
Tiếp theo, chúng ta thêm hai dòng ở giữa. Ở dòng đầu, ta gọi hàm rand::thread_rng
để lấy generator ngẫu nhiên mà ta sẽ dùng: một generator cục bộ cho luồng hiện tại và được seed bởi hệ điều hành. Sau đó ta gọi method gen_range
trên generator ngẫu nhiên. Method này do trait Rng
định nghĩa mà chúng ta đã đưa vào phạm vi bằng use rand::Rng;
. gen_range
nhận một biểu thức range làm đối số và sinh một số ngẫu nhiên trong range đó. Dạng range chúng ta dùng ở đây có dạng start..=end
và bao gồm cả hai đầu, nên ta phải chỉ 1..=100
để lấy số từ 1 đến 100.
Note: Bạn sẽ không phải lúc nào cũng biết trait nào dùng và method hay hàm nào gọi từ một crate, nên mỗi crate có tài liệu chỉ dẫn cách dùng. Một tính năng hay của Cargo là chạy cargo doc --open
sẽ xây dựng tài liệu của mọi dependency cục bộ và mở nó trong trình duyệt. Nếu bạn quan tâm các chức năng khác của crate rand
, ví dụ, chạy cargo doc --open
và nhấp rand
trong sidebar bên trái.
Dòng mới thứ hai in số bí mật. Điều này hữu ích trong khi phát triển để có thể test, nhưng chúng ta sẽ xóa nó trong phiên bản cuối. Trò chơi không còn vui nếu chương trình in đáp án ngay khi bắt đầu!
Thử chạy chương trình vài lần:
$ 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
Bạn sẽ nhận được các số ngẫu nhiên khác nhau, và tất cả đều trong khoảng từ 1 đến 100. Tuyệt!
Comparing the Guess to the Secret Number
Bây giờ chúng ta có đầu vào người dùng và một số ngẫu nhiên, ta có thể so sánh chúng. Bước này được thể hiện ở Listing 2-4. Lưu ý mã này hiện chưa biên dịch được, như ta sẽ giải thích.
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!"),
}
}
Đầu tiên chúng ta thêm một câu lệnh use
khác, đưa một kiểu tên std::cmp::Ordering
vào phạm vi từ standard library. Kiểu Ordering
là một enum khác và có các variant Less
, Greater
, và Equal
. Đây là ba kết quả có thể xảy ra khi so sánh hai giá trị.
Rồi chúng ta thêm năm dòng mới ở dưới dùng kiểu Ordering
. Method cmp
so sánh hai giá trị và có thể gọi trên bất cứ thứ gì có thể so sánh. Nó nhận một reference tới giá trị muốn so sánh với: ở đây là so sánh guess
với secret_number
. Sau đó nó trả về một variant của enum Ordering
mà ta đã đưa vào phạm vi bằng use
. Ta dùng một biểu thức match
để quyết định làm gì tiếp theo dựa trên variant Ordering
trả về từ cmp
khi so sánh guess
và secret_number
.
Một biểu thức match
gồm các arms. Một arm bao gồm một pattern để so khớp, và mã sẽ chạy nếu giá trị đưa cho match
phù hợp với pattern đó. Rust lấy giá trị đưa cho match
và so sánh theo thứ tự từng arm. Patterns và cấu trúc match
là các tính năng mạnh mẽ của Rust: chúng cho phép bạn diễn tả nhiều tình huống và đảm bảo bạn xử lý đầy đủ. Những tính năng này sẽ được trình bày trong Chương 6 và Chương 19.
Hãy đi qua một ví dụ với biểu thức match
ta dùng ở đây. Giả sử người dùng đoán 50 và số bí mật ngẫu nhiên lần này là 38.
Khi mã so sánh 50 với 38, method cmp
sẽ trả về Ordering::Greater
vì 50 lớn hơn 38. Biểu thức match
nhận giá trị Ordering::Greater
và bắt đầu kiểm tra pattern của từng arm. Nó nhìn arm đầu tiên với pattern Ordering::Less
và thấy Ordering::Greater
không khớp Ordering::Less
, nên bỏ qua arm đó và chuyển sang arm kế. Pattern arm kế là Ordering::Greater
, và nó khớp với Ordering::Greater
! Mã trong arm đó sẽ thực thi và in Too big!
ra màn hình. Biểu thức match
kết thúc sau khi tìm khớp đầu tiên, nên sẽ không xem arm cuối cùng trong kịch bản này.
Tuy nhiên, mã ở Listing 2-4 vẫn chưa biên dịch. Hãy thử:
$ 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/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/core/src/cmp.rs:964: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
Lỗi chính là mismatched types. Rust có hệ thống kiểu mạnh và tĩnh. Tuy nhiên, nó cũng có suy luận kiểu. Khi viết let mut guess = String::new()
, Rust suy luận guess
là String
và không bắt phải ghi kiểu. secret_number
, ngược lại, là một kiểu số. Một vài kiểu số trong Rust có thể có giá trị giữa 1 và 100: i32
, một số 32-bit; u32
, một số 32-bit không dấu; i64
, 64-bit; cũng như các kiểu khác. Nếu không chỉ định, Rust mặc định là i32
, đó là kiểu của secret_number
nếu không có thông tin kiểu khác. Lý do lỗi là Rust không thể so sánh một String
với một kiểu số.
Cuối cùng, chúng ta muốn chuyển String
mà chương trình đọc được sang kiểu số để có thể so sánh số học với số bí mật. Ta làm điều đó bằng cách thêm dòng này vào thân hàm 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!"),
}
}
Dòng là:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
Chúng ta tạo biến tên guess
. Nhưng khoan, chương trình đã có một biến tên guess
rồi? Đúng, nhưng may mắn là Rust cho phép ta shadow giá trị trước đó của guess
bằng một giá trị mới. Shadowing cho phép tái sử dụng tên biến guess
thay vì bắt phải tạo hai biến khác nhau, ví dụ guess_str
và guess
. Chúng ta sẽ trình bày chi tiết hơn ở Chương 3, nhưng hiện tại, biết rằng tính năng này thường dùng khi bạn muốn chuyển một giá trị từ kiểu này sang kiểu khác.
Chúng ta gán biến mới này bằng biểu thức guess.trim().parse()
. guess
trong biểu thức tham chiếu đến biến guess
ban đầu chứa đầu vào dạng chuỗi. Method trim
trên String
loại bỏ khoảng trắng ở đầu và cuối, điều cần thiết trước khi chuyển chuỗi sang u32
, vì u32
chỉ chứa ký tự số. Người dùng phải nhấn enter để read_line
nhận dự đoán, điều này thêm ký tự newline vào chuỗi. Ví dụ, nếu người dùng gõ 5 và nhấn enter, guess
trông như 5\n
. \n
biểu diễn “newline.” (Trên Windows, nhấn enter tạo carriage return và newline, \r\n
.) trim
loại bỏ \n
hoặc \r\n
, chỉ còn 5
.
Method parse
trên chuỗi chuyển chuỗi sang kiểu khác. Ở đây, ta dùng nó để chuyển từ chuỗi sang số. Ta cần cho Rust biết chính xác kiểu số mong muốn bằng cách ghi let guess: u32
. Dấu hai chấm (:
) sau guess
báo Rust rằng chúng ta chú thích kiểu cho biến. Rust có vài kiểu số; u32
ở đây là một số nguyên 32-bit không dấu. Đây là lựa chọn hợp lý cho một số nguyên dương nhỏ. Bạn sẽ học về các kiểu số khác trong Chương 3.
Thêm nữa, chú thích u32
trong ví dụ này và việc so sánh với secret_number
khiến Rust suy luận secret_number
cũng nên là u32
. Vì vậy bây giờ so sánh sẽ là giữa hai giá trị cùng kiểu!
Method parse
chỉ hoạt động trên các ký tự có thể hợp lý chuyển thành số và vì vậy có thể gây lỗi. Ví dụ, nếu chuỗi chứa A👍%
, sẽ không thể chuyển thành số. Vì có thể thất bại, parse
trả về một kiểu Result
, tương tự như read_line
. Ta sẽ xử lý Result
này bằng expect
như trước. Nếu parse
trả về Err
vì không thể tạo số, expect
sẽ làm chương trình dừng và in thông báo. Nếu parse
chuyển chuỗi thành số thành công, nó sẽ trả về Ok
và expect
sẽ trả giá trị số mà ta cần.
Hãy chạy chương trình giờ:
$ 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!
Tuyệt! Dù có khoảng trắng trước dự đoán, chương trình vẫn nhận ra người dùng đoán 76. Chạy vài lần để kiểm tra hành vi với các kiểu đầu vào: đoán đúng, đoán lớn hơn, đoán nhỏ hơn.
Chúng ta đã có hầu hết trò chơi hoạt động, nhưng người dùng chỉ có một lần đoán. Hãy thay đổi bằng cách thêm vòng lặp!
Allowing Multiple Guesses with Looping
Từ khóa loop
tạo một vòng lặp vô hạn. Chúng ta sẽ thêm một loop để cho người dùng thêm cơ hội đoán:
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!"),
}
}
}
Như bạn thấy, chúng ta đã di chuyển mọi thứ từ prompt nhập dự đoán trở đi vào trong một loop. Hãy chắc chắn thụt lề các dòng trong loop thêm bốn khoảng trắng và chạy chương trình lại. Chương trình bây giờ sẽ hỏi dự đoán liên tục, điều này giới thiệu một vấn đề mới: có vẻ như người dùng không thể thoát!
Người dùng luôn có thể ngắt chương trình bằng phím tắt ctrl-c. Nhưng có một cách khác để thoát, như đã đề cập ở phần parse
trong “Comparing the Guess to the Secret Number”: nếu người dùng nhập một giá trị không phải số, chương trình sẽ bị crash. Ta có thể tận dụng điều đó để cho phép người dùng thoát, như sau:
$ 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
Gõ quit
sẽ thoát trò chơi, nhưng như bạn thấy, nhập bất kỳ chuỗi không phải số nào cũng làm thoát. Điều này không tối ưu; ta muốn chương trình cũng dừng khi đoán đúng.
Quitting After a Correct Guess
Hãy lập trình để trò chơi thoát khi người dùng thắng bằng cách thêm câu lệnh 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;
}
}
}
}
Thêm dòng break
sau You win!
làm chương trình thoát vòng lặp khi người dùng đoán chính xác. Thoát vòng lặp cũng có nghĩa là thoát chương trình, vì loop là phần cuối của main
.
Handling Invalid Input
Để tinh chỉnh hành vi hơn nữa, thay vì làm chương trình crash khi người dùng nhập không phải số, ta hãy làm trò chơi bỏ qua đầu vào không hợp lệ để người dùng tiếp tục đoán. Ta có thể làm điều đó bằng cách thay đổi dòng nơi guess
được chuyển từ String
sang u32
, như trong 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;
}
}
}
}
Chúng ta chuyển từ gọi expect
sang một biểu thức match
để từ việc crash khi lỗi sang xử lý lỗi. Nhớ rằng parse
trả về Result
và Result
là enum có các variant Ok
và Err
. Ở đây ta dùng match
, như đã sử dụng với kết quả Ordering
của method cmp
.
Nếu parse
có thể chuyển chuỗi thành số, nó sẽ trả về Ok
chứa số đó. Giá trị Ok
này sẽ khớp với pattern Ok(num)
ở arm đầu, và biểu thức match
sẽ chỉ trả về giá trị num
mà parse
tạo ra và gán vào biến guess
mới.
Nếu parse
không thể chuyển chuỗi thành số, nó sẽ trả Err
chứa thông tin lỗi. Giá trị Err
sẽ không khớp Ok(num)
ở arm đầu, nhưng nó sẽ khớp Err(_)
ở arm hai. Dấu gạch dưới _
là một catch-all; ở đây ta nói rằng muốn khớp mọi Err
, bất kể thông tin bên trong. Vì vậy chương trình sẽ thực thi mã ở arm hai, continue
, nghĩa là đi sang vòng lặp tiếp theo và yêu cầu một dự đoán khác. Vậy là chương trình bỏ qua mọi lỗi mà parse
có thể gặp!
Giờ mọi thứ trong chương trình nên hoạt động như mong đợi. Hãy thử:
$ 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!
Tuyệt vời! Với một tinh chỉnh nhỏ cuối cùng, chúng ta sẽ hoàn thành trò chơi đoán số. Nhớ rằng chương trình vẫn in số bí mật. Điều đó hữu ích để test, nhưng làm hỏng trò chơi. Hãy xóa println!
in số bí mật. Listing 2-6 cho thấy mã hoàn chỉnh.
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;
}
}
}
}
Tại thời điểm này, bạn đã xây dựng thành công trò chơi đoán số. Xin chúc mừng!
Summary
Dự án này là cách thực hành giới thiệu nhiều khái niệm Rust mới: let
, match
, functions, việc dùng crate ngoài, và nhiều hơn nữa. Trong vài chương kế tiếp, bạn sẽ học các khái niệm này chi tiết hơn. Chương 3 trình bày các khái niệm mà hầu hết ngôn ngữ lập trình có, như biến, kiểu dữ liệu, và hàm, và cho thấy cách dùng chúng trong Rust. Chương 4 khám phá ownership, một tính năng làm Rust khác biệt. Chương 5 thảo luận structs và cú pháp method, và Chương 6 giải thích cách enums hoạt động.