2020年2月23日

Rust Guessing Game

一個基本的猜數字範例程式:程式會產生一個 1 ~ 100 的亂數,提示我們輸入數字猜猜看,填寫數字會得到太大或太小的結果,猜對時就結束遊戲。

Project

首先產生一個空的 guessting game project

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

因為 src/main.rs 有基本的 hello world 範例,故新的專案都可以直接執行

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/idea/rust/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.39s
     Running `target/debug/guessing_game`
Hello, world!

Processing a Guess

首先要處理 user 輸入的數字

rust 預設只會匯入 prelude,如果沒有在 prelude 就要用 use 匯入該函式庫

// 匯入 io library
use std::io;

// main() 函式是程式的進入點
// fn 語法宣告一個新函式,() 表示沒有傳入的參數,{ 後面開始是函式的內容
// 因為沒有回傳型別,所以這裡的回傳會是 (),一個空的 tuple
fn main() {
    // println! 是列印字串的巨集
    println!("Guess the number!");

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

    // let 是變數跟數值的綁定,預設是 immutable
    // 這裡加上 mut,變成 mutable variable
    // String 是 UTF-8 編碼的可變長度字串型別
    let mut guess = String::new();

    // io::stdin() 是使用 std::io::stdin(),會回傳一個 stdin 的 handle
    // 因最前面有 use std:io,所以這裡可省略 std::
    // 呼叫該 handle 的 read_line method,並提供 guess 變數的 reference

    // read_line() 會把使用者的輸入資料放入 &mut String 參數中。而它會回傳一個值 io::Result
    // Rust 的標準函式庫中有很多 Result 的型別:有一般的 Result 以及子函式庫的特別版本,ex: io::Result
    // Result 是要將錯誤訊息編碼
    // Result 的 variants 是 Ok, Err, Ok 裡面有成功的結果, Err 裡面有錯誤的原因

    // io:Result 提供 expect 方法,可取得呼叫該 method 的結果,如果結果不成功,就會 panic! ,並加上後面的 msg 訊息內容
    // 去掉expect 仍然可以編譯程式,但會出現 warning
    io::stdin().read_line(&mut guess).expect("Failed to read line");

    // 列印 guess 變數的資料, {} 是 placeholder
    println!("You guessed: {}", guess);
}

執行

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/idea/rust/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.70s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
1
You guessed: 1

Generating a Secret Number

Rust 標準函式庫沒有亂數的功能,但有提供 rand crate,crate 是一包 rust 的程式碼,而 rand 是 library crate,包含了可被其他城市使用的 codes。

要使用 crate 必須修改 Cargo.toml,在 dependencies 裡面加上 rand = "0.4.6"

[package]
name = "guessing_game"
version = "0.1.0"
authors = ["name <email@domain>"]

[dependencies]
rand = "0.4.6"

0.4.6 的部分稱為 Semantic Versioning (有時稱為SemVer),是標準的版本號碼,0.4.6 等同 ^0.4.6,就是任何跟 0.4.6 版本有相容 pulbic API的版本。如果要指定只能使用 0.4.6,要寫成 rand="=0.4.6"

再執行一次,會看到下載了新的 crate library,以及相依的 libc

$ cargo run
    Blocking waiting for file lock on the registry index
    Updating crates.io index
  Downloaded rand v0.3.23
  Downloaded rand v0.4.6
   Compiling libc v0.2.55
   Compiling rand v0.4.6
   Compiling rand v0.3.23
   Compiling guessing_game v0.1.0 (/Users/charley/project/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 20.87s
     Running `target/debug/guessing_game`

cargo 內建了一個機制,確保專案會使用相同版本的 dependencies,第一次建構專案後 (cargo build),就會產生一個 Cargo.lock檔案,該檔案裡面記錄了所有 dependencies 的版本號碼,接下來在重新 build project 時,會先檢查 Cargo.lock,並使用裡面的 versions of the dependencies。

如果要更新 crate 的版本,就要使用 cargo update 指令。

Generating a Random Number

使用 rand::Rng 產生亂數

use std::io;
// Rng 有產生亂數的 methods
use rand::Rng;

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

    // 1 是下限,101 是 exclusive upper bound
    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);
}

Comparing the Guess to the Secret Number

use std::io;
// Ordering 也是一個 Enum,成員是 Less, Greater, Equal
use std::cmp::Ordering;
use rand::Rng;

fn main() {

    // ---snip---

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

    // 把 guess 與 secret_number 做比較
    // 目前這裡會發生 mismatched types 錯誤,因為 Rust 是 strong type language
    // Rust 推測 guess 是 String,而 secret_number 是數字(i32/u32),預設為 i32
    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

編譯錯誤

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:22:21
   |
22 |     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`.

要修改程式,將 guess 轉換為數字

// --snip--

    let mut guess = String::new();

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

    // 重新定義 guess,trim 可去掉前後的空白或換行符號
    // 現在 guess 變成 u32,而 Rust 也會將 secret_number 視為 u32
    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!"),
    }
}           

加上 loop 重複猜測數字

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

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

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

}

目前程式還需要再猜對時,正常跳出程式。另外要針對 guess 的轉型進行檢查。

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

    // 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");

        // 將 expect 換成 match
        // parse 返回一個 Result 類型,而 Result 是一個 Ok 或 Err 成員的 enum
        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;
            },
        }
    }

}

References

The Rust Programming Language

中文版

中文版 2

沒有留言:

張貼留言