Lập Trình Một Trò Chơi Đoán Số

Hãy cùng bắt đầu làm quen với Rust thông qua một dự án thực tế! Chương này sẽ giới thiệu cho bạn một số khái niệm phổ biến trong Rust bằng cách hướng dẫn bạn sử dụng chúng trong một chương trình thực tế. Bạn sẽ học về let, match, phương thức, hàm liên kết, crate bên ngoài 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. Trong chương này, bạn chỉ cần thực hành những kiến thức cơ bản.

Chúng ta sẽ triển khai một bài tập lập trình cổ điển dành cho người mới bắt đầu: trò chơi đoán số. Trò chơi hoạt động như sau: chương trình sẽ tạo ra một số nguyên ngẫu nhiên từ 1 đến 100. Sau đó, chương trình sẽ yêu cầu người chơi nhập vào một số để đoán. Sau khi nhập số đoán, chương trình sẽ cho biết số đoán đó là quá nhỏ hay quá lớn. Nếu số đoán chính xác, trò chơi sẽ in ra thông báo chúc mừng và kết thúc.

Thiết Lập Một Dự Án Mới

Để thiết lập một dự án mới, hãy đi đến thư mục projects mà bạn đã tạo trong 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, lấy tên của dự án (guessing_game) làm đối số đầu tiên. Lệnh thứ hai sẽ chuyển đến thư mục của dự án mới.

Hãy xem file Cargo.toml đã được tạo ra:

Filename: Cargo.toml

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

[dependencies]

Như bạn đã thấy trong Chương 1, cargo new tạo ra 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 cách sử dụ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 lại nhanh chóng trên một dự án, như chúng ta sẽ làm trong trò chơi này, nhanh chóng kiểm tra từng lần lặp trước khi chuyển sang lần tiếp theo.

Mở lại file src/main.rs. Bạn sẽ viết tất cả mã trong file này.

Xử Lý Một Số Đoán

Phần đầu tiên của chương trình trò chơi đoán số sẽ yêu cầu người dùng nhập vào, xử lý đầu vào đó và kiểm tra xem đầu vào có đúng dạng mong đợi hay không. Để bắt đầu, chúng ta sẽ cho phép người chơi nhập vào một số đoán. Nhập mã trong 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 rất nhiều thông tin, vì vậy hãy đi qua từng dòng một. Để lấy đầu vào từ người dùng và sau đó in kết quả ra màn hình, chúng ta cần đưa thư viện đầu vào/đầu ra io vào phạm vi. Thư viện io đến từ thư viện tiêu chuẩn, được 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 thư viện tiêu chuẩn mà nó đưa vào phạm vi của mọi chương trình. Tập hợp này được gọi là prelude, và bạn có thể xem tất cả trong tài liệu thư viện tiêu chuẩn.

Nếu một kiểu bạn muốn sử dụng không có 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. Sử dụng thư viện std::io cung cấp cho bạn một số 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 trong Chương 1, hàm main là điểm vào của 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; dấu ngoặc đơn, (), chỉ ra rằng không có tham số; và dấu ngoặc nhọn, {, bắt đầu thân của hàm.

Như bạn cũng đã học trong 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 đang in ra một lời nhắc cho biết trò chơi là gì và yêu cầu đầu vào từ người dùng.

Lưu Trữ Giá Trị Với Biến

Tiếp theo, chúng ta sẽ tạo một biến để lưu trữ đầu vào từ 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 đang trở nên thú vị! Có rất nhiều điều đang diễn ra trong dòng nhỏ này. Chúng ta sử 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 ra một biến mới có tên là apples và gán nó với giá trị 5. Trong Rust, các biến mặc định là không thay đổi, có nghĩa là một khi chúng ta gán giá trị cho biến, giá trị đó sẽ không thay đổi. Chúng ta sẽ thảo luận chi tiết về khái niệm này trong phần “Biến và Tính Không Thay Đổi” trong Chương 3. Để làm cho một biến có thể thay đổi, chúng ta thêm mut trước tên biến:

let apples = 5; // không thay đổi
let mut bananas = 5; // có thể thay đổi

Lưu ý: Cú pháp // bắt đầu một bình luận kéo dài đến cuối dòng. Rust bỏ qua mọi thứ trong bình luận. Chúng ta sẽ thảo luận chi tiết về bình luận trong Chương 3.

Quay lại chương trình trò chơi đoán số, bây giờ bạn đã biết rằng let mut guess sẽ giới thiệu một biến có thể thay đổi có tên là guess. Dấu bằng (=) cho Rust biết chúng ta muốn gán một cái gì đó cho biến ngay bây giờ. Ở bên phải của dấu bằng là giá trị mà guess được gán, đó là kết quả của việc gọi String::new, một hàm trả về một phiên bản mới của một String. String là một kiểu chuỗi được cung cấp bởi thư viện tiêu chuẩn, là một đoạn văn bản có thể mở rộng, được mã hóa UTF-8.

Cú pháp :: trong dòng ::new chỉ ra rằng new là một hàm liên kết của kiểu String. Một hàm liên kết là một hàm được triển khai trên một kiểu, trong trường hợp này là String. Hàm new này tạo ra một chuỗi mới, trống. Bạn sẽ tìm thấy một hàm new trên nhiều kiểu vì nó là một tên phổ biến cho một hàm tạo ra một giá trị mới của một loại nào đó.

Tóm lại, dòng let mut guess = String::new(); đã tạo ra một biến có thể thay đổi hiện đang được gán với một phiên bản mới, trống của một String. Whew!

Nhận Đầu Vào Từ Người Dùng

Nhớ lại rằng chúng ta đã bao gồm chức năng đầu vào/đầu ra từ thư viện tiêu chuẩn với use std::io; ở dòng đầu tiên của chương trình. Bây giờ chúng ta sẽ gọi hàm stdin từ module io, điều này sẽ cho phép chúng ta 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 không nhập module io với use std::io; ở đầu chương trình, chúng ta vẫn có thể sử dụng hàm này bằng cách viết lời gọi hàm này là std::io::stdin. Hàm stdin trả về một phiên bản của std::io::Stdin, là một kiểu đại diện cho một tay cầm đến đầu vào tiêu chuẩn cho terminal của bạn.

Tiếp theo, dòng .read_line(&mut guess) gọi phương thức read_line trên tay cầm đầu vào tiêu chuẩn để lấy đầu vào từ người dùng. Chúng ta cũng đang truyền &mut guess làm đối số cho read_line để cho nó biết chuỗi nào để lưu trữ đầu vào từ người dùng. Công việc đầy đủ của read_line là lấy bất cứ thứ gì người dùng nhập vào đầu vào tiêu chuẩn và thêm vào một chuỗi (mà không ghi đè nội dung của nó), vì vậy chúng ta truyền chuỗi đó làm đối số. Đối số chuỗi cần phải có thể thay đổi để phương thức có thể thay đổi nội dung của chuỗi.

Dấu & chỉ ra rằng đối số này là một tham chiếu, điều này cho bạn một cách để cho nhiều phần của mã truy cập vào một phần dữ liệu mà không cần phải sao chép dữ liệu đó vào bộ nhớ nhiều lần. Tham chiếu là một 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 sử dụng tham chiếu an toàn và dễ dàng. Bạn không cần phải biết nhiều chi tiết đó để hoàn thành chương trình này. Hiện tại, tất cả những gì bạn cần biết là, giống như các biến, các tham chiếu mặc định là không thay đổi. Do đó, bạn cần viết &mut guess thay vì &guess để làm cho nó có thể thay đổi. (Chương 4 sẽ giải thích tham chiếu chi tiết hơn.)

Xử Lý Khả Năng Thất Bại Với Result

Chúng ta vẫn đang làm việc trên dòng mã này. Chúng ta hiện đang thảo luận về dòng văn bản thứ ba, nhưng lưu ý rằng nó vẫn là một phần của một dòng mã logic duy nhất. Phần tiếp theo là phương thứ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}");
}

Chúng ta có thể đã viết mã này như sau:

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

Tuy nhiên, một dòng dài rất khó đọc, vì vậy tốt nhất là chia nó ra. Thường thì nên giới thiệu một dòng mới và các khoảng trắng khác để giúp chia nhỏ các dòng dài khi bạn gọi một phương thức với cú pháp .method_name(). Bây giờ hãy thảo luận về những gì dòng này làm.

Như đã đề cập trước đó, read_line đặt bất cứ thứ gì người dùng nhập vào chuỗi mà chúng ta truyền cho nó, nhưng nó cũng trả về một giá trị Result. Result là một liệt kê, thường được gọi là enum, là một kiểu có thể ở một trong nhiều trạng thái có thể. Chúng ta gọi mỗi trạng thái có thể là một biến thể.

Chương 6 sẽ bao gồm các enum chi tiết hơn. Mục đích của các kiểu Result này là để mã hóa thông tin xử lý lỗi.

Các biến thể của ResultOkErr. Biến thể Ok chỉ ra rằng hoạt động đã thành công, và nó chứa giá trị được tạo ra thành công. Biến thể Err có nghĩa là hoạt động đã thất bại, và nó chứa thông tin về cách hoặc lý do tại sao hoạt động thất bại.

Các giá trị của kiểu Result, giống như các giá trị của bất kỳ kiểu nào, có các phương thức được định nghĩa trên chúng. Một phiên bản của Result có một phương thức expect mà bạn có thể gọi. Nếu phiên bản này của Result là một giá trị Err, expect sẽ làm cho chương trình bị sập và hiển thị thông báo mà bạn đã truyền làm đối số cho expect. Nếu phương thức read_line trả về một Err, nó có thể là kết quả của một lỗi đến từ hệ điều hành cơ bản. Nếu phiên bản này của Result là một giá trị Ok, expect sẽ lấy giá trị trả về mà Ok đang giữ và trả về chỉ giá trị đó cho bạn để bạn có thể sử dụng nó. 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 sẽ 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, chỉ ra rằng chương trình chưa xử lý một 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 của chúng ta, chúng ta chỉ muốn làm cho chương trình này bị sập khi có vấn đề xảy ra, vì vậy chúng ta có thể sử dụng expect. Bạn sẽ học về cách khôi phục từ lỗi trong Chương 9.

In Giá Trị Với println! Placeholder

Ngoài dấu ngoặc nhọn đóng, chỉ còn một dòng nữa để thảo luận trong mã cho đến nay:

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 ra chuỗi hiện chứa đầu vào của người dùng. Bộ {} của dấu ngoặc nhọn là một placeholder: hãy nghĩ về {} như những cái kẹp nhỏ giữ một giá trị tại chỗ. Khi in giá trị của một biến, tên biến có thể được đặt bên trong dấu ngoặc nhọn. Khi in kết quả của việc đánh giá một biểu thức, đặt dấu ngoặc nhọn trống trong chuỗi định dạng, sau đó theo sau chuỗi định dạng với một danh sách các biểu thức được phân tách bằng dấu phẩy để in trong mỗi placeholder dấu ngoặc nhọn trống theo cùng thứ tự. 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! sẽ trông như thế này:

#![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.

Kiểm Tra Phần Đầu Tiên

Hãy kiểm tra phần đầu tiên của trò chơi đoán số. Chạy nó bằng cách sử dụ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 tiên của trò chơi đã hoàn thành: chúng ta đang nhận đầu vào từ bàn phím và sau đó in nó ra.

Tạo Một Số Bí Mật

Tiếp theo, chúng ta cần tạo ra một số bí mật mà người dùng sẽ cố gắng đoán. Số bí mật nên khác nhau mỗi lần để trò chơi thú vị hơn khi chơi nhiều lần. Chúng ta sẽ sử dụng một số ngẫu nhiên từ 1 đến 100 để trò chơi không quá khó. Rust chưa bao gồm chức năng tạo số ngẫu nhiên trong thư viện tiêu chuẩn của nó. Tuy nhiên, nhóm Rust cung cấp một crate rand với chức năng này.

Sử Dụng Một Crate Để Có Thêm Chức Năng

Nhớ rằng một crate là một tập hợp các file mã nguồn Rust. Dự án mà chúng ta đã xây dựng là một binary crate, là một tệp thực thi. Crate rand là một library crate, chứa mã được sử dụng trong các chương trình khác và không thể thực thi độc lập.

Sự phối hợp của Cargo với các crate bên ngoài là nơi Cargo thực sự tỏa sáng. Trước khi chúng ta có thể viết mã sử dụng rand, chúng ta cần sửa đổi file Cargo.toml để bao gồm crate rand làm phụ thuộc. Mở file đó ngay bây giờ và thêm dòng sau vào cuối, bên dưới phần tiêu đề [dependencies] mà Cargo đã tạo cho bạn. Hãy chắc chắn chỉ định rand chính xác như chúng tôi đã làm ở đây, với số phiên bản này, hoặc các ví dụ mã trong hướng dẫn này 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 tiêu đề là một phần của phần đó tiếp tục cho đến khi một phần khác bắt đầu. Trong [dependencies] bạn nói với Cargo những crate bên ngoài mà dự án của bạn phụ thuộc vào và các phiên bản của các crate đó mà bạn yêu cầu. Trong trường hợp này, chúng ta chỉ định crate rand với chỉ định phiên bản ngữ nghĩa 0.8.5. Cargo hiểu Semantic Versioning (đôi khi được gọi là SemVer), là một tiêu chuẩn để viết số phiên bản. Chỉ định 0.8.5 thực sự là viết tắt của ^0.8.5, có nghĩa là bất kỳ phiên bản nào ít nhất là 0.8.5 nhưng dưới 0.9.0.

Cargo coi các phiên bản này có API công khai tương thích với phiên bản 0.8.5, và chỉ định này đảm bảo bạn sẽ nhận được bản phát hành vá lỗi mới nhất mà vẫn biên dịch được với mã trong chương này. Bất kỳ phiên bản nào từ 0.9.0 trở lên không được đảm bảo có cùng API như những gì các ví dụ sau đây sử dụng.

Bây giờ, mà không thay đổi bất kỳ mã nào, hãy xây dựng dự án, như được hiển thị 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

Bạ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 thuộc vào hệ điều hành), và các dòng có thể ở một thứ tự khác.

Khi chúng ta bao gồm một phụ thuộc bên ngoài, Cargo sẽ lấy các phiên bản mới nhất của mọi thứ mà phụ thuộc đó cần từ registry, là một bản sao của dữ liệu từ Crates.io. Crates.io là nơi mọi người trong hệ sinh thái Rust đăng các dự án Rust mã nguồn mở của họ để người khác sử dụng.

Sau khi cập nhật registry, Cargo kiểm tra phần [dependencies] và tải xuống bất kỳ crate nào được liệt kê mà chưa được tải xuống. Trong trường hợp này, mặc dù chúng ta chỉ liệt kê rand làm phụ thuộc, Cargo cũng đã lấy các crate khác mà rand phụ thuộc vào để hoạt động. Sau khi tải xuống các crate, Rust biên dịch chúng và sau đó biên dịch dự án với các phụ thuộc có sẵn.

Nếu bạn ngay lập tức chạy cargo build lại mà không thực hiện bất kỳ thay đổi nào, bạn sẽ không nhận được bất kỳ kết quả nào ngoài dòng Finished. Cargo biết rằng nó đã tải xuống và biên dịch các phụ thuộc, và bạn chưa thay đổi bất kỳ điều gì về chúng trong file Cargo.toml của bạn. Cargo cũng biết rằng bạn chưa thay đổi bất kỳ điều gì về mã của bạn, vì vậy nó không biên dịch lại mã đó. Không có gì để làm, nó chỉ đơn giản thoát ra.

Nếu bạn mở file src/main.rs, thực hiện một thay đổi nhỏ và sau đó lưu nó và xây dựng lại, bạn sẽ chỉ thấy hai dòng kết quả:

$ 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 rằng Cargo chỉ cập nhật bản dựng với thay đổi nhỏ của bạn đối với file src/main.rs. Các phụ thuộc của bạn không thay đổi, vì vậy Cargo biết rằng nó có thể tái sử dụng những gì nó đã tải xuống và biên dịch cho những phụ thuộc đó.

Đảm Bảo Các Bản Dựng Có Thể Tái Tạo Với File Cargo.lock

Cargo có một cơ chế đảm bảo bạn có thể tái tạo cùng một artifact mỗi khi bạn hoặc bất kỳ ai khác xây dựng mã của bạn: Cargo sẽ chỉ sử dụng các phiên bản của các phụ thuộc mà bạn đã chỉ định cho đến khi bạn chỉ định khác. Ví dụ, giả sử rằng tuần tới phiên bản 0.8.6 của crate rand ra mắt, và phiên bản đó chứa một bản sửa lỗi quan trọng, nhưng nó cũng chứa một lỗi hồi quy sẽ làm hỏng mã của bạn. Để xử lý điều này, Rust tạo file Cargo.lock lần đầu tiên bạn chạy cargo build, vì vậy bây giờ chúng ta có file này trong thư mục guessing_game.

Khi bạn xây dựng một dự án lần đầu tiên, Cargo sẽ tìm ra tất cả các phiên bản của các phụ thuộc phù hợp với tiêu chí và sau đó ghi chúng vào file Cargo.lock. Khi bạn xây dựng dự án của mình trong tương lai, Cargo sẽ thấy rằng file Cargo.lock tồn tại và sẽ sử dụng các phiên bản được chỉ định ở đó thay vì làm tất cả công việc tìm ra các phiên bản lại. Điều này cho phép bạn có một bản dựng có thể tái tạo tự động. Nói cách khác, dự án của bạn sẽ vẫn ở phiên bản 0.8.5 cho đến khi bạn nâng cấp rõ ràng, nhờ vào file Cargo.lock. Vì file Cargo.lock quan trọng đối với các bản dựng có thể tái tạo, nó thường được kiểm tra vào kiểm soát nguồn cùng với phần còn lại của mã trong dự án của bạn.

Cập Nhật Một Crate Để Nhận Phiên Bản Mới

Khi bạn muốn cập nhật một crate, Cargo cung cấp lệnh update, lệnh này sẽ bỏ qua file Cargo.lock và tìm ra tất cả các phiên bản mới nhất phù hợp với các chỉ định của bạn trong Cargo.toml. Cargo sau đó sẽ ghi các phiên bản đó vào file Cargo.lock. Trong trường hợp này, Cargo sẽ chỉ tìm các phiên bản lớn hơn 0.8.5 và nhỏ hơn 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 điều sau nếu bạn 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 phiên bản 0.9.0. Tại thời điểm này, bạn cũng sẽ nhận thấy một thay đổi trong file Cargo.lock ghi nhận rằng phiên bản của crate rand mà bạn đang sử dụng bây giờ là 0.8.6. Để sử dụng phiên bản 0.9.0 của rand hoặc bất kỳ phiên bản nào trong loạt 0.9.x, bạn sẽ phải cập nhật file Cargo.toml để trông như thế này:

[dependencies]
rand = "0.9.0"

Lần tiếp theo bạn chạy cargo build, Cargo sẽ cập nhật registry của các crate có sẵn và đánh giá lại các yêu cầu của bạn về rand theo phiên bản mới mà bạn đã chỉ định.

Có rất nhiều điều để nói về Cargohệ sinh thái của nó, mà chúng ta sẽ thảo luận trong Chương 14, nhưng hiện tại, đó là tất cả những gì bạn cần biết. Cargo làm cho việc tái sử dụng các thư viện rất dễ dàng, vì vậy các Rustaceans có thể viết các dự án nhỏ hơn được lắp ráp từ một số gói.

Tạo Một Số Ngẫu Nhiên

Hãy bắt đầu sử dụng rand để tạo ra một số để đoán. Bước tiếp theo là cập nhật src/main.rs, như được hiển thị 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 phương thức mà các bộ tạo số ngẫu nhiên triển khai, và trait này phải nằm trong phạm vi để chúng ta sử dụng các phương thức đó. Chương 10 sẽ bao gồm các trait chi tiết.

Tiếp theo, chúng ta thêm hai dòng ở giữa. Trong dòng đầu tiên, chúng ta gọi hàm rand::thread_rng để lấy bộ tạo số ngẫu nhiên cụ thể mà chúng ta sẽ sử dụng: một bộ tạo số ngẫu nhiên cục bộ cho luồng thực thi hiện tại và được khởi tạo bởi hệ điều hành. Sau đó, chúng ta gọi phương thức gen_range trên bộ tạo số ngẫu nhiên. Phương thức này được định nghĩa bởi trait Rng mà chúng ta đã đưa vào phạm vi với câu lệnh use rand::Rng;. Phương thức gen_range lấy một biểu thức phạm vi làm đối số và tạo ra một số ngẫu nhiên trong phạm vi đó. Loại biểu thức phạm vi mà chúng ta đang sử dụng ở đây có dạng start..=end và bao gồm cả giới hạn dưới và giới hạn trên, vì vậy chúng ta cần chỉ định 1..=100 để yêu cầu một số từ 1 đến 100.

Lưu ý: Bạn sẽ không chỉ biết các trait nào để sử dụng và các phương thức và hàm nào để gọi từ một crate, vì vậy mỗi crate có tài liệu với hướng dẫn sử dụng nó. Một tính năng thú vị khác của Cargo là chạy lệnh cargo doc --open sẽ xây dựng tài liệu được cung cấp bởi tất cả các phụ thuộc của bạn cục bộ và mở nó trong trình duyệt của bạn. Nếu bạn quan tâm đến các chức năng khác trong crate rand, ví dụ, hãy chạy cargo doc --open và nhấp vào rand trong thanh bên trái.

Dòng mới thứ hai in ra số bí mật. Điều này hữu ích trong khi chúng ta đang phát triển chương trình để có thể kiểm tra nó, nhưng chúng ta sẽ xóa nó khỏi phiên bản cuối cùng. Nó không phải là một trò chơi nếu chương trình in ra câu trả lời ngay khi nó bắt đầu!

Hãy thử chạy chương trình một 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 nên nhận được các số ngẫu nhiên khác nhau, và tất cả chúng nên là các số từ 1 đến 100. Làm tốt lắm!

So Sánh Số Đoán Với Số Bí Mật

Bây giờ chúng ta đã có đầu vào từ người dùng và một số ngẫu nhiên, chúng ta có thể so sánh chúng. Bước đó được hiển thị trong Listing 2-4. Lưu ý rằng mã này sẽ không biên dịch ngay lập tức, như chúng 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 gọi là std::cmp::Ordering vào phạm vi từ thư viện tiêu chuẩn. Kiểu Ordering là một enum khác và có các biến thể Less, GreaterEqual. Đây là ba kết quả có thể xảy ra khi bạn so sánh hai giá trị.

Sau đó, chúng ta thêm năm dòng mới ở cuối sử dụng kiểu Ordering. Phương thức cmp so sánh hai giá trị và có thể được gọi trên bất kỳ thứ gì có thể so sánh. Nó lấy một tham chiếu đến bất kỳ thứ gì bạn muốn so sánh: ở đây nó đang so sánh guess với secret_number. Sau đó, nó trả về một biến thể của enum Ordering mà chúng ta đã đưa vào phạm vi với câu lệnh use. Chúng ta sử dụng một biểu thức match để quyết định làm gì tiếp theo dựa trên biến thể nào của Ordering được trả về từ lời gọi đến cmp với các giá trị trong guesssecret_number.

Một biểu thức match được tạo thành từ các nhánh. Một nhánh bao gồm một mẫu để so khớp, và mã sẽ được chạy nếu giá trị được đưa vào match phù hợp với mẫu của nhánh đó. Rust lấy giá trị được đưa vào match và xem qua mẫu của từng nhánh lần lượt. Các mẫu 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 biểu đạt nhiều tình huống mà mã của bạn có thể gặp phải và chúng đảm bảo bạn xử lý tất cả chúng. Các tính năng này sẽ được bao gồm chi tiết trong Chương 6 và Chương 19, tương ứng.

Hãy đi qua một ví dụ với biểu thức match mà chúng ta sử dụng ở đây. Giả sử rằng người dùng đã đoán 50 và số bí mật được tạo ngẫu nhiên lần này là 38.

Khi mã so sánh 50 với 38, phương thức 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 mẫu của từng nhánh. Nó xem mẫu của nhánh đầu tiên, Ordering::Less, và thấy rằng giá trị Ordering::Greater không phù hợp với Ordering::Less, vì vậy nó bỏ qua mã trong nhánh đó và chuyển sang nhánh tiếp theo. Mẫu của nhánh tiếp theo là Ordering::Greater, điều này phù hợp với Ordering::Greater! Mã liên kết trong nhánh đó sẽ được thực thi và in ra Too big! trên màn hình. Biểu thức match kết thúc sau khi khớp thành công đầu tiên, vì vậy nó sẽ không xem nhánh cuối cùng trong tình huống này.

Tuy nhiên, mã trong Listing 2-4 sẽ không biên dịch ngay lập tức. Hãy thử nó:

$ 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 cốt lõi cho biết rằng có các kiểu không khớp. Rust có một hệ thống kiểu mạnh mẽ và tĩnh. Tuy nhiên, nó cũng có suy luận kiểu. Khi chúng ta viết let mut guess = String::new(), Rust có thể suy luận rằng guess nên là một String và không bắt chúng ta phải viết kiểu. Mặt khác, secret_number là một kiểu số. Một vài kiểu số của Rust có thể có giá trị từ 1 đến 100: i32, một số 32-bit; u32, một số 32-bit không dấu; i64, một số 64-bit; cũng như các kiểu khác. Trừ khi được chỉ định khác, Rust mặc định là một i32, đó là kiểu của secret_number trừ khi bạn thêm thông tin kiểu ở nơi khác để Rust suy luận một kiểu số khác. Lý do cho lỗi là Rust không thể so sánh một chuỗi và một kiểu số.

Cuối cùng, chúng ta muốn chuyển đổi String mà chương trình đọc làm đầu vào thành một kiểu số để chúng ta có thể so sánh nó về mặt số học với số bí mật. Chúng 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 này là:

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

Chúng ta tạo một biến có tên là guess. Nhưng chờ đã, chương trình đã có một biến có tên là guess chưa? Đúng vậy, nhưng may mắn thay Rust cho phép chúng ta che bóng giá trị trước đó của guess bằng một giá trị mới. Che bóng cho phép chúng ta tái sử dụng tên biến guess thay vì buộc chúng ta phải tạo hai biến duy nhất, chẳng hạn như guess_strguess, ví dụ. Chúng ta sẽ bao gồm điều này chi tiết hơn trong Chương 3, nhưng hiện tại, hãy biết rằng tính năng này thường được sử dụng khi bạn muốn chuyển đổi một giá trị từ một kiểu sang một kiểu khác.

Chúng ta gán biến mới này với biểu thức guess.trim().parse(). guess trong biểu thức đề cập đến biến guess ban đầu chứa đầu vào dưới dạng chuỗi. Phương thức trim trên một phiên bản String sẽ loại bỏ bất kỳ khoảng trắng nào ở đầu và cuối, điều mà chúng ta phải làm trước khi có thể chuyển đổi chuỗi thành một u32, chỉ có thể chứa dữ liệu số. Người dùng phải nhấn enter để thỏa mãn read_line và nhập số đoán của họ, điều này thêm một ký tự xuống dòng vào chuỗi. Ví dụ, nếu người dùng nhập 5 và nhấn enter, guess trông như thế này: 5\n. \n đại diện cho “xuống dòng.” (Trên Windows, nhấn enter sẽ dẫn đến một ký tự xuống dòng và một ký tự trở về, \r\n.) Phương thức trim loại bỏ \n hoặc \r\n, chỉ để lại 5.

Phương thức parse trên chuỗi chuyển đổi một chuỗi thành một kiểu khác. Ở đây, chúng ta sử dụng nó để chuyển đổi từ một chuỗi thành một số. Chúng ta cần nói với Rust kiểu số chính xác mà chúng ta muốn bằng cách sử dụng let guess: u32. Dấu hai chấm (:) sau guess cho Rust biết rằng chúng ta sẽ chú thích kiểu của biến. Rust có một vài kiểu số tích hợp; u32 được thấy ở đây là một số nguyên 32-bit không dấu. Đó là một lựa chọn mặc định tốt cho một số dương nhỏ. Bạn sẽ học về các kiểu số khác trong Chương 3.

Ngoài ra, chú thích u32 trong chương trình ví dụ này và so sánh với secret_number có nghĩa là Rust sẽ suy luận rằng secret_number cũng nên là một u32. Vì vậy, bây giờ so sánh sẽ là giữa hai giá trị cùng kiểu!

Phương thức parse sẽ chỉ hoạt động trên các ký tự có thể chuyển đổi hợp lý thành số và do đó có thể dễ dàng gây ra lỗi. Nếu, ví dụ, chuỗi chứa A👍%, sẽ không có cách nào để chuyển đổi điều đó thành một số. Vì nó có thể thất bại, phương thức parse trả về một kiểu Result, giống như phương thức read_line (được thảo luận trước đó trong “Xử Lý Khả Năng Thất Bại Với Result). Chúng ta sẽ xử lý Result này theo cùng cách bằng cách sử dụng phương thức expect một lần nữa. Nếu parse trả về một biến thể Err của Result vì nó không thể tạo ra một số từ chuỗi, lời gọi expect sẽ làm cho trò chơi bị sập và in ra thông báo mà chúng ta đưa ra. Nếu parse có thể chuyển đổi thành công chuỗi thành một số, nó sẽ trả về biến thể Ok của Result, và expect sẽ trả về số mà chúng ta muốn từ giá trị Ok.

Hãy chạy chương trình ngay bây 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 vời! Mặc dù đã thêm các khoảng trắng trước số đoán, chương trình vẫn nhận ra rằng người dùng đã đoán 76. Chạy chương trình một vài lần để xác minh hành vi khác nhau với các loại đầu vào khác nhau: đoán số chính xác, đoán một số quá lớn và đoán một số quá nhỏ.

Chúng ta đã có hầu hết trò chơi hoạt động, nhưng người dùng chỉ có thể đoán một lần. Hãy thay đổi điều đó bằng cách thêm một vòng lặp!

Cho Phép Nhiều Lần Đoán Với Vòng Lặp

Từ khóa loop tạo ra một vòng lặp vô hạn. Chúng ta sẽ thêm một vòng lặp để cho phép người dùng có nhiều cơ hội đoán số hơ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 có thể thấy, chúng ta đã di chuyển mọi thứ từ lời nhắc nhập số đoán trở đi vào một vòng lặp. Hãy chắc chắn thụt lề các dòng bên trong vòng lặp thêm bốn khoảng trắng mỗi dòng và chạy chương trình lại. Chương trình bây giờ sẽ yêu cầu một số đoán khác mãi mãi, điều này thực sự giới thiệu một vấn đề mới. Dường như người dùng không thể thoát ra!

Người dùng luôn có thể ngắt chương trình bằng cách sử dụng phím tắt ctrl-c. Nhưng có một cách khác để thoát khỏi con quái vật không thể thỏa mãn này, như đã đề cập trong phần thảo luận về parse trong “So Sánh Số Đoán Với Số Bí Mật”: nếu người dùng nhập một câu trả lời không phải là số, chương trình sẽ bị sập. Chúng ta có thể tận dụng điều đó để cho phép người dùng thoát ra, như được hiển thị ở đây:

$ 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

Nhập quit sẽ thoát khỏi trò chơi, nhưng như bạn sẽ nhận thấy, việc nhập bất kỳ đầu vào không phải là số nào khác cũng sẽ thoát ra. Điều này là không tối ưu, ít nhất là; chúng ta muốn trò chơi cũng dừng lại khi số đoán chính xác.

Thoát Sau Khi Đoán Đúng

Hãy lập trình trò chơi để thoát khi người dùng thắng bằng cách thêm một 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 cho chương trình thoát khỏi vòng lặp khi người dùng đoán đúng số bí mật. Thoát khỏi vòng lặp cũng có nghĩa là thoát khỏi chương trình, vì vòng lặp là phần cuối cùng của main.

Xử Lý Đầu Vào Không Hợp Lệ

Để tinh chỉnh thêm hành vi của trò chơi, thay vì làm cho chương trình bị sập khi người dùng nhập vào một số không phải là số, hãy làm cho trò chơi bỏ qua số không phải là số để người dùng có thể tiếp tục đoán. Chúng ta có thể làm điều đó bằng cách thay đổi dòng mà guess được chuyển đổi từ một String thành một u32, như được hiển thị 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ừ một lời gọi expect sang một biểu thức match để chuyển từ việc làm cho chương trình bị sập khi có lỗi sang xử lý lỗi. Nhớ rằng parse trả về một kiểu ResultResult là một enum có các biến thể OkErr. Chúng ta đang sử dụng một biểu thức match ở đây, như chúng ta đã làm với kết quả Ordering của phương thức cmp.

Nếu parse có thể chuyển đổi thành công chuỗi thành một số, nó sẽ trả về một giá trị Ok chứa số kết quả. Giá trị Ok đó sẽ khớp với mẫu của nhánh đầu tiên, và biểu thức match sẽ chỉ trả về giá trị numparse đã tạo ra và đặt bên trong giá trị Ok. Số đó sẽ kết thúc ngay tại nơi chúng ta muốn trong biến guess mới mà chúng ta đang tạo.

Nếu parse không thể chuyển đổi chuỗi thành một số, nó sẽ trả về một giá trị Err chứa thêm thông tin về lỗi. Giá trị Err không khớp với mẫu Ok(num) trong nhánh đầu tiên của match, nhưng nó khớp với mẫu Err(_) trong nhánh thứ hai. Dấu gạch dưới, _, là một giá trị bắt tất cả; trong ví dụ này, chúng ta đang nói rằng chúng ta muốn khớp với tất cả các giá trị Err, bất kể thông tin nào chúng có bên trong. Vì vậy, chương trình sẽ thực thi mã của nhánh thứ hai, continue, điều này cho chương trình biết đi đến lần lặp tiếp theo của loop và yêu cầu một số đoán khác. Vì vậy, về cơ bản, chương trình bỏ qua tất cả các lỗi mà parse có thể gặp phải!

Bây giờ mọi thứ trong chương trình nên hoạt động như mong đợi. Hãy thử nó:

$ 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 đang in ra số bí mật. Điều đó hoạt động tốt cho việc kiểm tra, nhưng nó làm hỏng trò chơi. Hãy xóa println! in ra số bí mật. Listing 2-6 hiển thị 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ố. Chúc mừng!

Tóm Tắt

Dự án này là một cách thực hành để giới thiệu cho bạn nhiều khái niệm mới trong Rust: let, match, hàm, việc sử dụng các crate bên ngoài và nhiều hơn nữa. Trong các chương tiếp theo, bạn sẽ học về các khái niệm này chi tiết hơn. Chương 3 bao gồm các khái niệm mà hầu hết các ngôn ngữ lập trình đều có, chẳng hạn như biến, kiểu dữ liệu và hàm, và chỉ cho bạn cách sử dụng chúng trong Rust. Chương 4 khám phá quyền sở hữu, một tính năng làm cho Rust khác biệt so với các ngôn ngữ khác. Chương 5 thảo luận về cấu trúc và cú pháp phương thức, và Chương 6 giải thích cách hoạt động của các enum.