0%

[Rust]教學系列-猜謎遊戲:最終篇

前情提要

來到「猜謎遊戲」的最終篇章了,前面兩篇[Rust]教學系列-猜謎遊戲:取得使用者輸入值[Rust]教學系列-猜謎遊戲:取亂數,我們已經把前置作業該做的都做完了,最終篇就是重頭戲中的重頭戲,將取得的使用者輸入值與隨機產生的亂數做一個比較,若一樣則比賽結束,若不一致則可以讓使用者繼續猜,跟著筆者一起實作吧。

筆者先預告一下此篇主要是會使用到Rust的重要方法match以及若使用者輸入值與神秘數字不一樣時,可以讓使用者一直猜下去的loop技巧。

內容

比較數字

需要引入一些基礎模組套件並使用

1
2
3
4
5
6
7
use std::cmp::Ordering;

match guess.cmp(&secret_number){
Ordering::Less => println!("太小了!"),
Ordering::Greater => println!("太大了!"),
Ordering::Equal => println!("獲勝!")
}

可以發現上述的程式碼是無法建置的,是因為guess為使用者輸入值,是一個String型態,secret_number則是透過gen_range產生的亂數,是數字型態,兩邊資料型別不對等的情況下,是無法比較的。VSCode中的Rust ExtensionRust Language Server服務,會偵測到錯誤並提示

1
2
3
mismatched types

expected struct `std::string::String`, found integer

意即以第一個比較對象為基準,因為guess為使用者輸入值,資料型別理所當然是String型態,因此透過cmp方法比較的兩邊,資料型別要一致的狀態下,會跳出stringstring的提示。

解釋match方法結構,筆者找不出更直白的方式敘述,以下兩段敘述引用官方教學文件中的翻譯敘述:

match表達式由分支(arms)所組成。分支包含一個模式(pattern)以及對應的程式碼,這在當match表達式開頭的數值能與該分支的模式配對時就能執行。Rust 會用match得到的數值依序遍歷每個分支中的模式。match結構與模式是 Rust 中非常強大的特色,能讓你表達各種程式碼可能會遇上的情形,並確保你有將它們全部處理完。這些特色功能會在第六章與第十八章分別討論其細節。

讓我們看看在此例中使用match表達式時會發生什麼事。假設使用者猜測的數字是 50 而這次隨機產生的祕密數字是 38。當程式碼比較 50 與 38 時,cmp方法會回Ordering::Greater,因為 50 大於 38。match表達式會取得Ordering::Greater數值並開始檢查每個分支的模式。它會先查看第一個分支的模Ordering::Less並看出數值Ordering::Greater無法與Ordering::Less配對,所以它忽略該分支的程式碼,並移到下一個分支。而下個分支的模Ordering::Greater能配對到Ordering::Greater!所以該分支對應的程式碼就會執行並印出太大了!到螢幕上。最後match表達式就會結束,因為在此情境中它已經不需要再查看最後一個分支。

看完上述敘述,給筆者一種感覺很像其他語言中的switch case,畢竟matchRust語言的世界中屬流程控制,筆者認為應該就是筆者熟悉的C#世界中的switch case,官方教學文件也會有一篇專門介紹match的章節,到時再跟著筆者慢慢體會其作法及威力吧。

字串轉型

接著就需要用到字串轉型的技巧了,因為畢竟我是猜數字遊戲,需要當作數值做比較才有其意義,因此將使用者輸入值guess(String型別)轉型成可以跟secret_number可以比較的型態

1
let guess: u32 = guess.trim().parse().expect("請輸入一個數字!");

看到這裡,有沒有一種感覺,這什麼鬼 XD,上面已經有guess變數了,可以再宣告一個guess嗎?這是Rust語言的特色之一:shadow遮蔽,因為有這個特色,我們可以重複使用變數名稱,且右邊的guess就是第一個String型別的資料,經過運算之後指派給新的變數guess,這個概念之後會有專門一篇說明。

程式裡面使用到trim().parse(),經過[上篇]的說明,應該知道回傳是一個泛型Result型別,因此可以再接著使用expect這個方法,筆者試看看若command line輸入的是不是數字時的反應

1
2
3
4
5
6
7
8
9
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
請猜測一個數字!
祕密數字為:96
請輸入你的猜測數字!
oooo # 故意輸入一串英文字母
# 錯誤訊息:自定義的訊息+error訊息
thread 'main' panicked at '請輸入一個數字!: ParseIntError { kind: InvalidDigit }', src/main.rs:17:43
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

階段性執行結果

寫到這邊,已經完成一個可執行的猜謎遊戲了

1
2
3
4
5
6
7
8
cargo run
# 輸出結果
請猜測一個數字!
祕密數字為:30
請輸入你的猜測數字!
50 # 手動輸入
太大了!
你的猜測數字:50

當然以執行結果來說,是不是覺得不夠完美,有以下幾個項目可以改善

  • 不應該印出祕密數字
  • 轉換資料型別失敗時的處理
  • 若猜錯,是否可以再繼續進行猜數字,直到猜中為止

迴圈設計

筆者這邊解釋一下為何要加入迴圈設計

  • 依照轉型失敗時的處理,勢必要讓使用者再輸入一次,直到可以將其值成功轉換成數字型別
  • 猜錯數字,必須得讓使用者再猜一次

基於上述兩個理由,必須使用迴圈將主要邏輯包住,可以重複執行同樣的邏輯,Rust語言的迴圈設計則使用loop關鍵字,將需要重複執行的程式邏輯區塊包住即可有效果,另外搭配

  • continue:中間有邏輯符合則執行到最後,直接進入下一個迴圈
  • break:已達終止條件,終止迴圈

完整程式碼

筆者就不賣關子了,直接把上章節說的不完美的地方加入迴圈設計後的改善結果吧,講一下程式實作邏輯

  • 產生1-100間的亂數
  • loop開始
    • 取得使用者輸入值
    • 將使用者輸入值轉換資料型別為數值(若轉換型別錯誤則讓loop繼續)
    • 資料比較邏輯:若猜中,終止loop

最後列出完整程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("請猜測一個數字!");
let secret_number = rand::thread_rng().gen_range(1..101);

loop {
println!("請輸入你的猜測數字!");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("讀取該行失敗");

let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("你的猜測數字:{}", guess);

match guess.cmp(&secret_number) {
Ordering::Less => println!("太小了!"),
Ordering::Greater => println!("太大了!"),
Ordering::Equal => {
println!("獲勝!");
break;
}
};
}
}

執行結果

筆者自己也來玩一場吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cargo run
# 輸出結果
請猜測一個數字!
請輸入你的猜測數字!
50
你的猜測數字:50
太大了!
請輸入你的猜測數字!
25
你的猜測數字:25
太大了!
請輸入你的猜測數字!
15
你的猜測數字:15
太小了!
請輸入你的猜測數字!
22
你的猜測數字:22
太小了!
請輸入你的猜測數字!
23
你的猜測數字:23
獲勝!

結論

做完有趣的題目,下篇開始要無聊的介紹資料型別基礎設計概念等觀念,還有一些Rust獨有的概念原則,開始一一揭開Rust神秘的面紗,筆者希望可以講完基礎概念後,可以實作一些Side Project,讓自己可以更加靈活的運用Rust語言。最後筆者Recap一下今天的一些重要觀念

  • match的用法:很像其他語言中的switch case,屬流程控制的一環
  • parse方法:轉型方法,回傳泛型Result型別
  • loop技巧:包住想要重複執行的程式邏輯區塊,搭配continuebreak使用

參考