猜數字遊戲
這篇學到的事:
- 學會接收使用者輸入值
- 引用外部 crate
- 寫註解
- 變數型別轉換
- 使用 match, shadow, enum, expect()
0. 遊戲規則
產生一個 1 到 100 間的亂數。 然後提示我們輸入數字猜猜看
當我們輸入之後,它會告訴我們太大還是太小。 當我們猜對了,它會恭喜我們
1. 建立專案
在終端機執行
cargo new guessing_game --bin
加入 --bin
的參數,因為我們要建立執行檔而不是函式庫
使用 cargo 產生新專案的時候,預設就在 src/main.rs
幫你產生 hello world,讓你可以直接測試
於是就直接使用 cargo build
來編譯一下吧!還記得之前說過 Rust 的編譯跟執行是分開的嗎?
所以在編譯好後需要另外執行 cargo run
來執行剛剛編譯好的檔案,這樣就可以看見初始化專案時候建立的 hello world 顯示在終端機視窗上了
2. 取得使用者輸入
改寫 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);
}
use 引用
use std::io;
取得標準函式庫中的 io
函式庫
main()
fn main() {
因為 main 沒有回傳值,所以他的回傳值是一個空的 tuple 「()
」
接著印出了兩行說明
變數定義
let mut guess = String::new();
不一樣的東西來了,有一個 let 陳述式,它被用來建立「變數綁定」
它的形式是:let foo = bar;
,很多程式語言會這麼做,但在 Rust 中事情有些不同
let 變數預設是不可變的(immutable),使用
mut
使綁定變成可變的(mutable),而不再是不可變的
範例:
let foo = 5; // immutable.
let mut bar = 5; // mutable
藉著範例你也知道了在 Rust 中註解是使用兩個斜線 //
跟你熟悉的程式語言類似嗎?
我們知道了等號左邊,那接著來了解右邊的 String::new()
String
String 是一個字串型別,由標準函式庫提供
::new()
語法使用 ::
是因為它是一個特定型別的「關聯函式」(associated function)。也就是說,它被關聯到 String 本身,而非特定的某個 String 的實體(instance)。
一些語言(例如 PHP)稱之為「靜態方法」(static method)
new()
會建立一個新的、空的 String
io
io::stdin().read_line(&mut guess)
.expect("Failed to read line");
因為我們第一行有 use std::io;
所以這邊可以直接呼叫關聯函式,否則我們就要把這行寫成 std::io::stdin()
。這個函式會回傳終端機標準輸入的控制代碼(handle),詳細說明可以參考std::io::Stdin
.read_line(&mut guess)
接著使用這個控制代碼(handle)呼叫(handle)的 read_line() 方法取得使用者的輸入
接著好玩的事情就發生了,在前面我們已經把 guess 設定成可變的(mutable)。然而,read_line 不接受把 String 當作參數:它只接受 &mut String
Rust 有一個叫做 參照(references)的功能,它允許你將多個參照指向同一塊資料,這樣可以降低複製的動作,畢竟 Rust 的賣點就是安全。這個和 PHP 中的參照(Reference),或是中國那邊翻作引用。使用「&」符號來賦值相似
因為 guess
是使用者輸入的字串,所以他必須是一個可變的變數
然而這行程式並還未結束,記得嗎?我們說過要看到分號才算表達式完成。這邊換行是為了人類閱讀上方便,當然也可以寫成一行
.expect("Failed to read line");
這段主要在做的事情是把錯誤處理訊息編碼,上面的 read_line()
會把取得的值放入 &mut String
參數並回傳一個值,在這邊是一個 io::Result
為的是把錯誤處理訊息編碼,此處的 io::Result 有 expect() 方法,若是不成功就會回傳 Rust 內建的 panic!
panic! 會讓你的程式當機,並顯示出傳給它的訊息。
印出變數
println!("You guessed: {}", guess);
{}
是個 placeholder,因此我們傳遞 guess 作為參數。如果我們有多個 {},我們就需要傳遞多個參數,概念類似於 MySQL 中的 %s
用法
接著執行 cargo run
執行專案,就完成第一部分:從鍵盤取得輸入值,然後印出來。
3. 產生秘密數字
接著要產生隨機數字,但是 Rust 本身的標準函式庫還沒有包含亂數功能。不過 Rust 團隊提供了 rand crate 來協助我們產生隨機亂數
於是照著文件,我們在 Cargo.toml
加入所需的外部 crates 及版本
[dependencies]
rand = "0.7.3"
Cargo 套件的版本號能使用 Semantic Versioning 的標準方式
上面的版號實際上可以寫成 ^0.3.0
,代表「所有跟 0.3.0 相容的版本」。如果我們想要確實的使用 0.3.0,我們可以寫成 rand="=0.3.0"(請注意引號內還有一個等號),而當我們想要使用最新版,我們可以使用 *。
了解版本號的撰寫方式之後回頭來建置專案:cargo build
修改我們的程式碼
extern crate rand;
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);
}
可以發現在第一行,加入了 extern crate rand
讓 Rust 知道我們要使用 rand 這個 crate。等同於 use rand;
,所以我們可以透過 rand:: 前綴詞來使用任何 rand crate 內的東西。
接著使用剛剛加入的 crate rand 來取得指定範圍內的隨機數字,並且印出來。這只是簡單測試功能正常,畢竟猜數字是不該一開始就印出解答的
比較猜測值
接著就要來比較輸入跟答案的值,這邊會用到標準函式庫中一個比較大小的函式庫型別:cmp::Ordering;
extern crate rand;
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);
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!"),
}
}
可以看到最後面多了五行程式碼,cmp()
方法可以被任何能用來比較的東西呼叫,且它會要求傳入你想比較的東西的參照(記得嗎?上面講的 &)
它回傳我們前面 use 的 Ordering 型別,而我們使用了 match 陳述去判定它實際上是哪種 Ordering
Ordering 是個
enum
,是枚舉(enumeration)的簡寫
枚舉看起來會有點像這樣:
enum Foo {
Bar,
Baz,
}
這裡定義了一個 Foo 的命名空間(namespace),若使用裡面的變數,必須使用 ::
,例如 Foo::Bar 或是 Foo::Baz
解釋完 enum 就可以了解那五行在做什麼了
然後就給他 cargo run
下去!
error[E0308]: mismatched types
--> src/main.rs:23:21
|
23 | match guess.cmp(&secret_number) {
| ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integer
|
= note: expected reference `&std::string::String`
found reference `&{integer}`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game`.
出現問題了!
他說我們有一個 mismatched types
翻譯過來就是「無法匹配的型別」
原來是因為,我們的 guess 在定義的時候說他是個 String::new();
是個字串,但是 secret_number 有多種可以存入 1 到 100 值的型別,有:32 位元整數 i32、無號數(unsigned)32 位元整數 u32、64 位元整數 i64 或其他等等。
Rust 預設為 i32
但這都不是這邊的問題,而是 Rust 不知道怎麼去比對 guess 與 secret_number。他們必須是同樣的型別,這在動態語言如 PHP javascript 中是會被語言本身解決的,工程師不需要在型別上糾結,相對的也得付出一些代價
想要把輸入的 String 轉換為一個真正的數字型別,我們可以用額外的兩行做到
let guess: u32 = guess.trim().parse()
.expect("Please type a number!");
等等!我們已經有了 guess 了吧?
沒錯,但 Rust 允許我們用新的 guess 去「遮蔽」(shadow)前一個。通過使用 let
重新聲明一個已經存在的變數,這時是可以繼續使用原本變數所賦予的值,而不需要重新想一個變數來區分例如:guess_str 和 guess
一開始 guess 是 String,但我們希望能轉換為 u32,在類似的情況中我們時常使用這個方法
接著就是把原本的 guest 綁定在表達式上,最終要回傳 u32 的型別回來
guess.trim().parse()
值得一提的是因為我們必須按下「Enter」按鍵去符合 read_line() 的輸入條件。意思就是當我們輸入「87」按下 Enter 送出,那 guess 就會是:87\n。 \n 代表「新的一行」(newline)、enter 鍵。所以必須使用 trim()
來清除這些多餘的東西
而 字串的 parse() 方法會把字串分析為數字。因為它可以被分析為很多種數字型別,我們必須給 Rust 我們確切想要的數字型別的提示。所以我們在等號左邊寫了 let guess: u32
告訴 Rust 我們需要的型別
跟 read_line()
一樣,我們呼叫 parse()
時可能會發生錯誤,例如有人輸入了 A👍%
怎麼辦?沒錯就是跟 read_line()
一樣,使用 expect()
方法,讓他在出錯時停止
再編譯一次執行,果然這次就沒有問題了。但新的問題來了,我只能猜一次,這不符合遊戲規則。所以我們加入迴圈來改進他
迴圈
關鍵字 loop
會給我們一個無限迴圈,break
會幫助我們離開無窮迴圈
所以會在要求使用者輸入前開始無限迴圈,直到使用者猜的數字符合條件,那我們就在該條件下 break
指令讓程式跳出無窮迴圈
如果你實際的玩了幾次你的程式,並且耐不住好奇心驅使輸入了非數字的內容,你會發現 expect()
方法的確可以停止錯誤,但一般的遊戲不應該直接跳出,應該要告訴使用者該怎麼做
這是因為 expect()
是直接將程式「崩潰 (crash)」,於是我們把 parse()
修改為以下的樣子
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
改為使用 match
陳述的方式,把「錯誤時當機」改為「實際處理錯誤」的方法,parse()
回傳的 Result
是個跟 Ordering
類似的 enum
。但裡面的變數是 Ok
或是 Err
當 match 到 Ok(num) 時,會把 Ok 內的值設給 num 這個名稱,然後在右邊直接回傳它;在 Err 的情況,我們不在意發生了什麼錯誤,所以使用一個下劃線 _
來接收到 Err 收到的所有內容,不管其中包含什麼訊息。接著 continue 讓我們可以繼續 loop 的下一次疊代(iteration)要求使用者輸入數字。
看起來應該是沒問題了!執行最終的完整程式碼…
等等!先把印出答案那行刪掉
extern crate rand;
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 = 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;
}
}
}
}
留言
張貼留言