All Articles

Lập trình với Rust - Guessing Game

Phần này sẽ giới thiệu với bạn đọc một số khái niệm cơ bản của Rust thông qua một dự án thực. Bạn sẽ được học về let, match, method, function, crate. Phần này chúng ta sẽ chỉ làm việc với những phần cơ bản nhất. Chi tiết hơn sẽ được đề cập ở các chương tiếp theo.

Phần này sẽ hướng dẫn bạn tạo một chương trình cơ bản: Trò chơi đoán số. Nó đơn giản như thế này:

Chương trình tạo ra một số ngẫu nhiên từ 1 tới 100. Rồi nó sẽ hỏi người chơi đoán một số. Sau khi người dùng nhập số đoán đó, chương trình sẽ nói số đó to hơn hay bé hơn số được chọn. Đến khi con số trùng khớp, chương trình sẽ in ra thông báo chúc mừng.

Tạo dự án mới

$ cargo new guessing_game
     Created binary (application) `guessing_game` package
$ cd guessing_game

Lệnh cargo new sẽ tạo ra một dự án mới guesing_game. Bên trong có file Cargo.toml. Kiểm tra nội dung file đó, nếu thông tin sai thì sửa lại:

[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your name <your-email-address>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Như đã trình bày trong phần trước, cargo new cũng đồng thời tạo ra một file source code.

Tên file: src/main.rs

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

Bước đầu chạy thử với cargo run để biết chắc là chương trình hoạt động đúng.

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/phuong.nguyen/rust_projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 3.52s
     Running `target/debug/guessing_game`
Hello, world!

Thay đổi source code

Bước đầu tiên cho chương trình đoán số là yêu cầu người dùng nhập vào một số, xử lý input, kiểm tra input đúng định dạng. Sửa nội dung của file src/main.rs như sau:

Tên file: src/main.rs

Nội dung:

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

Giải thích đoạn code ở trên:

Để nhận input/output từ người dùng, chúng ta dùng thư viện io (viết tắt cho input/output). Thư viện io có trong bộ thư viện chuẩn std (standard):

use std::io;

Mặc định thì Rust chỉ cung cấp một vài thư viện sẵn (prelude). Ngoài ra nếu muốn thêm thư viện thì chúng ta cần thêm vào với từ khoá use. Thư viện std::io cung cấp một số tính năng hữu ích, bao gồm cả việc đọc input từ người dùng.

Cũng giống như trong phần trước, chúng ta biết rằng hàm main là nơi chương trình chạy khi bắt đầu:

fn main() {}

Từ khoá fn khai báo hàm. Dấu ngoặc () không có gì bên trong nói lên hàm không có parameter. Ngoặc nhọn { biển diễn bắt đầu của hàm.

println! là một macro in chuỗi ra màn hình:

println!("Guess the number!");

println!("Please input your guess.");

Lưu giá trị vào biến

Để lưu input vào biến:

let mut guess = String::new();

Từ khoá let thể hiện đó là một biến số. Một ví dụ khác về biến:

let foo = bar;

Trong Rust, mặc định tất cả các biến là immutable, nghĩa là không thay đổi giá trị được. Để thay đổi giá trị thì phải khai báo biện dạng mutable, với từ khoá mut.

let foo = 5; // immutable
let mut bar = 5; // mutable

// đi theo sau là comment.

Từ khoá :: trong dòng lệnh thể hiện new là một hàm liên kết với type String (associated function). Một hàm associated function gắn với type thay vì gắn với object. Trong một số ngôn ngữ khác thì kiểu hàm này được gọi là static function.

Hàm new tạo ra một chuỗi rỗng. Hàm new sẽ được thấy trong nhiều loại dữ liệu khác và thông dụng để tạo ra giá trị của một type nào đó.

Nói tóm tắt lại thì let mut guess = String::new(); tạo ra một chuỗi rỗng, gắn vào biến có thể thay đổi tên là guess.

Tiếp đến chúng ta sẽ đọc giá trị từ màn hình do người dùng nhập vào:

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

Vì chúng ta đã khai báo dùng thư viện use std::io nên chỉ cần gọi hàm io::stdin() thay vì viết đầy đủ std::io::stdin. Mặc dù cách viết nào cũng chạy được.

Hàm stdin khi chạy sẽ tạo ra một instance std::io::Stdin (Chú ý Stdin - viết hoa S) - nó là một đối tượng giúp ta nhận input từ terminal.

Đoạn code tiếp theo .read_line(&mut guess) - gọi method read_line để lấy input từ người dùng. Một tham số được truyền vào: &mut guess.

Ký tự & thể hiện tham số thuộc dạng reference - Nó cho phép chúng ta truy cập vào cùng một dữ liệu trong nhiều đoạn code khác nhau mà không cần phải copy vào memory. Reference là một trong những khái niệm khó và quan trọng của Rust.

Reference mặc định là immutable nên chúng ta cần dùng từ khoá mut để làm cho nó mutable.

Handle failure với Type Result

.expect("Failed to read line");

Khi gọi hàm liên tiếp, Rust có thể ngắt xuống dòng (newline) để code dễ hiểu. Có thể viết trên một dòng mà kết quả vẫn vậy:

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

Hàm read_line ngoài việc nhận giá trị của người dùng cho vào biến, nó còn trả lại một giá trị (return) - Trong trường hợp này là io::Result. Trong bộ thư viện chuẩn của Rust có một kiểu dữ liệu là Result.

Result bản chất là enums (hay enumeration). Cũng như enumeration trong các ngôn ngữ khác, nó có thể nhận giá trị trong một tập nào đó, các giá trị này được gọi là variants.

Trường hợp của Result có giá trị Ok hoặc Err. Chúng ta có thể tham khảo điều này từ tài liệu chuẩn trên trang web: https://doc.rust-lang.org/std/result/enum.Result.html

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

Nếu chung ta không thêm dòng .expect() thì khi chạy Rust sẽ báo warning:

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/phuong.nguyen/rust_projects/guessing_game)
warning: unused `std::result::Result` that must be used
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: #[warn(unused_must_use)] on by default
   = note: this `Result` may be an `Err` variant, which should be handled

    Finished dev [unoptimized + debuginfo] target(s) in 3.46s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
1
You guessed: 1

Như log ở trên, Rust warning chúng ta đã không dùng Result từ kết quả của read_line. Nghĩa là chương trình đã không kiểm tra lỗi có thể xảy ra.

Cách làm đúng là viết error handling, nhưng trường hợp này ta chỉ cần dừng chương trình khi xảy ra lỗi nên ở trên đã dùng hàm .expect().

In giá trị ra màn hình với placeholder của macro println!

Câu lệnh:

println!("You guessed: {}", guess);

Chúng ta đã dùng {} như placeholder. Nó sẽ được thay thể bới giá trị chúng ta muốn in, ở đây là biến guess.

Nếu muốn in nhiều giá trị ra thì làm như sau:

let x = 5;
let y = 10;

println!("x = {} and y = {}", x, y);

Đoạn code này khi chạy sẽ in ra x = 5 and y = 10.

Chạy thử đoạn code

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/phuong.nguyen/rust_projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.82s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
12
You guessed: 12

Tạo một con số bí ẩn

Chúng ta sẽ dùng phương pháp tạo random một con số mỗi khi chương trình chạy.

Dùng Crate

Crate là một tập hợp nhiều file source code Rust. Dự án mà chúng ta đang thực hiện được gọi là binary crate, tức là chương trình có thể chạy được. rand crate được gọi là library crate - nghĩa là code được dùng trong các chương trình khác.

Trước khi muốn dùng rand, chúng ta phải khai báo trong Cargo.toml như một thư viện ngoài, có viết rõ version.

Filename: Cargo.toml

[dependencies]

rand = "0.3.14"

Cách ký hiệu version tuân thủ theo chuẩn sematic versioning. Nghĩa là chúng ta có thể khai báo version dạng như là ^0.3.14. Nếu bạn quen thuộc với dự án NodeJS/npm/yarn thì cách viết version ở đây cũng tương tự như vậy.

Đầu tiên, build lại dự án để thêm thư viện:

$ cargo build
    Updating crates.io index
  Downloaded rand v0.7.0
  Downloaded getrandom v0.1.8
  Downloaded rand_core v0.5.0
  Downloaded cfg-if v0.1.9
  Downloaded rand_chacha v0.2.1
  Downloaded c2-chacha v0.2.2
  Downloaded lazy_static v1.3.0
  Downloaded ppv-lite86 v0.2.5
   Compiling cfg-if v0.1.9
   Compiling lazy_static v1.3.0
   Compiling ppv-lite86 v0.2.5
   Compiling getrandom v0.1.8
   Compiling rand_core v0.5.0
   Compiling c2-chacha v0.2.2
   Compiling rand_chacha v0.2.1
   Compiling rand v0.7.0
   Compiling guessing_game v0.1.0 (/Users/phuong.nguyen/rust_projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 12.50s

Thông thường crate được download từ crate registry, trường hợp này mặc định là từ Crates.io. Crates.io là nơi cộng đồng open source có thể đóng góp thư viện cho Rust.

Lệnh cargo build giúp chúng ta download các dependency. Trường hợp này do chúng khai báo dùng rand nên lệnh sẽ download rand và các dependency của chính rand.

Để biết version mới nhất của rand, kiểm tra trên Crates.io.

Cập nhật version mới cho crate

Khi muốn cập nhập version mới, dùng lệnh cargo update:

$ cargo update
    Updating crates.io index

Tạo số ngẫu nhiên

Filename: src/main.rs

use std::io;
use rand::Rng;

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

    let secret_number = rand::thread_rng().gen_range(1, 101);

    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);
}
cargo run
   Compiling guessing_game v0.1.0 (/Users/phuong.nguyen/rust_projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.96s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 16
Please input your guess.
16
You guessed: 16

Hàm rand::thread_rng sẽ sinh cho chúng ta number generator, từ đó tạo ra một số ngẫu nhiên với hàm gen_range. Hàm này nhận hai parameter, số ngẫu nhiên sẽ nằm giữa hai parameter này. Trong trường hợp ví dụ trên sẽ nằm giữa 1101.

Kiểm tra số nhập vào với số random được tạo

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {

    // ---snip---

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

Chạy sẽ báo lỗi:

cargo run
   Compiling guessing_game v0.1.0 (/Users/phuong.nguyen/rust_projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:21:21
   |
21 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integer
   |
   = note: expected type `&std::string::String`
              found type `&{integer}`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: Could not compile `guessing_game`.

To learn more, run the command again with --verbose.

Lỗi này có nghĩa là khi so sánh hai giá trị, type của chúng không khớp nhau. Trong khi số người dùng input vào thuộc dạng String, secrete_number lại là dạng integer.

Rust là ngôn ngữ static type, nên khi so sánh hai số với nhau, chúng phải cùng type.

Để giải quyết vấn đề này, chúng ta cần đồi type số nhập vào từ người dùng sang dạng integer.

Filename: src/main.rs

// --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!"),
    }
}

Chạy lại:

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/phuong.nguyen/rust_projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 75
Please input your guess.
75
You guessed: 75
You win!

Với đoạn code trên, chúng ta đã chuyển biến guess sang dạng u32:

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

Rust cho phép shadow biến, dù đã khai báo dạng String trước đó, chúng ta vẫn có thể khai báo lại chuyển sang dạng integer.

u32 nghĩa là unsigned 32-bit number.

Dùng lặp để lặp lại đoán số nếu sai

Filename: src/main.rs

// --snip--

    println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}
$ cargo run
   Compiling guessing_game v0.1.0 (/Users/phuong.nguyen/rust_projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.76s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 26
Please input your guess.
1
You guessed: 1
Too small!
Please input your guess.
50
You guessed: 50
Too big!
Please input your guess.
20
You guessed: 20
Too small!
Please input your guess.
27
You guessed: 27
Too big!
Please input your guess.
25
You guessed: 25
Too small!
Please input your guess.
26
You guessed: 26
You win!

Ở trên chúng ta đã dùng loop để tạo vòng lặp vô hạn cho người dùng nhập số.

Nếu trường hợp số nhập vào trùng với con số random tạo ra lúc đầu, dùng break để kết thúc chương trình.

Hanlde nhập liệu invalid

Trường hợp người dùng có thể nhập vào không phải số mà là một chuỗi ngẫu nhiên. Khi đó bình thường chương trình sẽ bị lỗi. Để khắc phúc thì có thể ignore giá trị nhập vào nếu không phải là số:

// --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--
$ cargo run
   Compiling guessing_game v0.1.0 (/Users/phuong.nguyen/rust_projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.78s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 71
Please input your guess.
lsjflsj
Please input your guess.
71
You guessed: 71
You win!

Như thấy ở trên, nếu người dùng nhập không phải là số, vòng lặp sẽ được continue chạy lại, yêu cầu người dùng nhập vào số.

Hàm parse ở trên cũng trả về một Result enum. Có thể là Ok hoặc Err. Nếu Ok thì trả về chính số đó, nếu Err thì xử lý continue.

Chương trình nói chung vậy là hoàn thiện. Có một điều là chúng ta đang in ra con số bí mật. Xoá dòng println! đó để hoàn thiện:

Filename: main.rs

Xoá dòng sau:

println!("The secret number is: {}", secret_number);

Chương trình hoàn thiện:

use std::io;
use std::cmp::Ordering;
use rand::Rng;

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

    let secret_number = rand::thread_rng().gen_range(1, 101);

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

Chạy thử:

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/phuong.nguyen/rust_projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.66s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
50
You guessed: 50
Too small!
Please input your guess.
75
You guessed: 75
Too small!
Please input your guess.
85
You guessed: 85
Too big!
Please input your guess.
80
You guessed: 80
You win!