在這個章節,我們將會直接使用Rust程式語言來建立出猜數字程式的專案,並逐步將它完成!並在撰寫程式的過程中,來練習Rust程式語言基礎的程式語法和引用外部套件的方式。這支小程式在執行之後,將會先從1到100的整數中,隨機抽取一個數字作為答案,並且允許使用者輸入要猜的數字,如果答錯了,程式會回答使用者輸入的數字究竟是大於答案,還是小於答案,並持續讓使用者繼續猜下去;如果答對了,程式會出現使用者贏了的訊息,並且結束程式。



建立新的Cargo專案

使用Cargo來建立出猜數字程式的專案,將其命名為guessing_game,指令如下:

cargo new --bin guessing_game

在這裡我們加了--bin參數,讓Cargo建立出可執行的應用程式專案。

接著就將工作移動到程式專案的根目錄吧!也就是剛才建立出來的guessing_game目錄。

如果您不放心程式專案可能沒有產生成功,那麼可以使用以下指令來直接執行剛才產生出來的guessing_game專案。

cargo run

就如同前一章所介紹的一樣,Cargo建立出來的可執行應用程式專案,就是Hello World程式。所以會看到程式在螢幕上印出了Hello, world!

rust-guess-number

修改專案設定檔

成功產生出Cargo的專案後,用文字編輯器打開Cargo.toml,應該會看到如以下的內容:

[package]                                                                       
name = "guessing_game"
version = "0.1.0"
edition = "2021"
 
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
[dependencies]

在套件的設定區域中,name欄位對應的就是專案名稱,Cargo的專案名稱習慣使用底線來將兩個單字區隔開來。我們在一開始下指令建立專案的時候,就已經將專案名稱取名為guessing_game了,所以這邊已經預先填好了guessing_game。如果想要改變專案名稱,直接改掉這個欄位的值即可。專案目錄的名稱可以跟專案名稱不相同。

再來,version欄位對應的是程式的版本,Cargo支援「語意化版本」(Semantic Versioning),版本的表示方式如下:

主版號(major).次版號(minor).修訂號(patch)

預設的新專案的程式版本號碼是從0.1.0開始,之後每做一次程式專案的修改,在發佈新版本的程式時,就應該要去修改這邊的版本號碼,將其按照底下的規則遞增(通常是加1):

  • 主版號:做了不相容的API修改。
  • 次版號:做了向下相容的功能性新增。
  • 修訂號:做了向下相容的問題修正。

如果要更講究使用版本號碼的話,詳細的語意化版本控制規範(SemVer)可以到底下這個網頁來查看:

edition欄位表示要使用的Rust程式語言的版本,版本以西元年份來表示,2015為初版,2018為第二版,2021為第三版。不同版本的Rust程式語言無法完全上下相容。

開始撰寫程式碼

設定好程式專案之後,就可以開始專心寫程式啦!

用文字編輯器開啟main.rs,將println!("Hello, world!");改成println!("Guess the number!");,目的是要讓程式在螢幕上印出Guess the number!這幾個字。

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

程式在螢幕上印出Guess the number!之後,應該要先出好猜數字的題目,也就是隨機從1到100的整數中挑出一個作為猜數字的答案。然而,Rust的標準函式庫,並沒有提供隨機數值的函數,因此在此我們需要引用外部的套件,來完成。至於要使用哪個套件呢?那就是官方提供的rand啦。

為了讓Cargo程式專案能夠使用外部的套件,我們必須編輯Cargo.toml專案設定檔,要在[dependencies]的區塊中加入rand套件,撰寫的格式如下:

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

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

[dependencies]
rand = "0.8.4"

注意Cargo.toml設定檔的第9行,我們加入了rand = "0.8.4"。等號左邊表示套件的名稱,等號右邊表示套件的版本。也就是說,在此我們會把「rand v0.8.4」這個套件加入至我們的專案中使用。根據語意化版本控制規範,如果將版本寫成^0.8.5,代表「任何API相容於0.8.4版本的版本」,Cargo會選擇當時最新且符合這個條件的套件來使用。舉例來說,如果之後rand套件有了三個新版本,分別是0.8.50.9.21.0.0,而在我們的Cargo.toml設定檔中,rand套件的版本是設定為^0.8.4的話,Cargo將會自動選擇0.8.5來使用,確保程式專案能用到比較新版本的套件,且也能夠在不用修改專案原本程式碼的情況下,依然可以成功編譯程式。可是,為什麼Cargo會是選擇0.8.5,而不是0.9.2呢?主版號一樣的話不是表示API都是相容的嗎?這其實是個例外狀況,當「主版號」為0的時候,表示這個套件還在開發階段,API會改來改去,但因為程式還在開發階段,不應該將「主版號」遞增到1以上,因此會退而求其次,只遞增「次版號」。這個例外規則稱作「Magic Zero」!

雖然剛才提到版本寫成^0.8.5的話就會有自動使用最新相容套件版本的功能,事實上,在Cargo.toml設定檔中,若將套件的版本直接寫成0.8.5,其實就代表^0.8.5了!

rand套件加入Cargo.toml設定檔後,請試試看用以下指令編譯專案:

cargo build

rust-guess-number

Cargo就會自動從crates.io將我們剛才加入至Cargo.toml設定檔的套件和其本身相依的套件都下載下來編譯了!

crates.io是Rust官方提供的一個能讓Rust程式設計者們分享開源套件的平台,rand套件就是crates.io上的其中一個套件,網址如下:

此時如果直接再執行一次cargo build,因為我們對專案沒有進行任何的修改,因此Cargo也不會花時間重新下載套件和編譯專案。

rust-guess-number

如果我們對main.rs進行一些改動,比如說,將輸出的Guess the number!,在最後多加一個!

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

此時再執行一次cargo build,會發現Cargo只會重新編譯剛才修改的main.rs檔案,而並未重新下載rand套件,也沒有對它進行重新編譯的動作。因為Cargo只重新編譯有做修改的部份,可以大大增加編譯速度。

rust-guess-number

在編譯專案之後,程式專案的根目錄中會產生出Cargo.lock檔案。這個檔案被用來記錄此Cargo程式專案使用的相依套件版本,假使之後這些套件有的發佈了API相容的新版本,Cargo在編譯專案時,還是會根據Cargo.lock裡面記載的套件版本,也就是最一開始編譯專案時選擇使用的套件版本,來繼續沿用那個版本的套件,而不是去使用新版本,如此一來也不用每次編譯專案都要去尋找和下載比較新的套件。但若要讓Cargo去使用API相容的新版本套件,也可以使用以下指令來更新Cargo.lock檔案:

cargo update

這裡要注意的是,cargo update只會更新Cargo.lock檔案,並不會去更新Cargo.toml設定檔內的套件版本哦!

知道怎麼加套件至Cargo程式專案後,再來就是要在程式碼中使用它啦!在上一章節有稍微提到,Rust對於套件有定義了一個新的名稱──「crate」(板條箱),若要在程式碼中使用套件,需在程式碼的最上方使用extern crate關鍵字。由於我們要使用的套件為rand,因此在程式碼第一行插入extern crate敘述,插入後的程式碼如下:

extern crate rand;

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

接下來介紹產生一個1到100隨機整數的方式。

extern crate rand;

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

    let secret_number = rand::thread_rng().gen_range(1..=100);
}

來看一下我們新增的第六行敘述。let這個關鍵字可以建立變數,例如:

let foo = bar;

這行程式可以宣告出一個變數foo,並且讓它存放bar這個值(value)。在Rust程式語言中,預設宣告出來的變數是不可以改變(immutable)的,舉例來說:

let x = 5;

如果要讓宣告出來的變數可以被改變(mutable),則需要在let關鍵字加上mut。程式碼如下:

let mut foo = bar;

舉例來說:

let x = 5; // immutable
let mut y = 5; // mutable

以上程式會宣告出兩個變數xy,並且讓它們都存放5這個數值。變數x由於在宣告時沒有使用mut關鍵字的關係,因此它只能夠被存取,無法進行修改,也就是說x的值永遠是5。而變數y在宣告時使用了mut關鍵字,因次在程式執行階段,變數y的值有可能從原本的5,被改成其它同型別的值。就像大多數的程式語言一樣,//也是Rust語言的單行註解開頭。加入註解只是為了方便理解程式碼,對於程式本身來說,註解並沒有任何用處,Rust編譯器會忽略所有程式碼中的註解,因此也不用擔心註解寫太多會造成編譯出來的程式體積變大。

回到main.rs程式碼的部份。

extern crate rand;

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

    let secret_number = rand::thread_rng().gen_range(1..=100);
}

在一般情況下,通常我們會省略不寫extern crate敘述。那麼,在什麼情況下必須要寫extern crate敘述呢?當我們需要使用Rust自帶的crate時就必須要,例如proc_macroallocstd,不過在預設情況下Rust會自己引入標準函式庫,所以我們其實也不太需要寫extern crate std;這樣的敘述。

總而言之,以上程式可以簡寫如下:

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

    let secret_number = rand::thread_rng().gen_range(1..=100);
}

在這個猜數字程式中,用來儲存猜數字答案的變數secret_number並不需要被改變,因此在被宣告出來時不使用mut關鍵字。如此一來可以確保我們在撰寫程式的時候不會不小心去改到這個變數的值,而編譯器也可以有機會對這種值不會被改變的變數進行特別優化。

接下來我們來看一下rand::thread_rng().gen_range(1..=100)到底是什麼意思吧!Rust程式語言使用::這樣的語法來尋找指定套件或是模組下的項目,像是這裡使用的rand::thread_rng(),表示要從rand套件中呼叫thread_rng這個函數。thread_rng這個函數會回傳一個ThreadRng結構(struct)的實體(instance),可以根據目前執行這個函數是用哪個執行緒,來決定要用什麼亂數種子(seed)。

接著看到thread_rng().gen_range(1..=100)。Rust程式語言使用.這樣的語法來使用結構實體的方法(method),因此這個敘述將會再去呼叫由thread_rng函數回傳的ThreadRng結構實體的gen_range方法。然而,事實上,原始的ThreadRng結構,並沒有實作gen_range這樣如此高階的方法,因此目前的程式是無法成功編譯的。

為了要讓程式能夠成功編譯,我們需要在程式碼中定義函數的上方,再加入一行敘述,程式碼如下:

use rand::Rng;

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

    let secret_number = rand::thread_rng().gen_range(1..=100);
}

第1行程式,我們使用了use這個關鍵字來使用rand套件所提供的RngRngrand套件已經實作好的特性(trait),簡單來說Rng這個特性能夠擴充ThreadRng結構所擁有的方法,使其擁有一些高階產生隨機數值的方法,gen_range方法就是其中之一。

gen_range方法需要傳入一個參數,來決定要從哪個範圍中隨機產生數值。這個參數要傳入一個範圍(range),在Rust程式語言中可以用最小值..最大值這類的語法來表示一個範圍,這裡要注意的是,若是只有寫..來連接最小值和最大值,則最大值本身並不在範圍內,要使用..=(多一個等於)來連接才可以使範圍包含最大值。因此,若要使用gen_range方法來隨機產生1到100中的一個的整數,程式碼需寫成gen_range(1..=100),或是gen_range(1..101)

您可能會注意到,我們將儲存猜數字答案的變數取名叫secret_number,而剛才用到的方法名稱則是thread_rnggen_range,這是Rust程式語言對於專案、變數、函數、方法的命名習慣──使用底線將單字區分開來,英文字母為小寫。而像是Rng等結構、特性、列舉(enum)、列舉值的命名習慣,則是使用駝峰式大小寫的方式區隔單字,且第一個字母為大寫。

有關於Rust的結構、特性和列舉將會在之後的章節詳細介紹,現階段只要知道執行gen_range方法之後,我們會得到指定範圍內的一個隨機數值。

緊接著我們可以將這個隨機產生出來的數值再利用println!這個巨集輸出到螢幕上。程式如下:

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

就像是其它程式語言有的字串格式化(format)功能,Rust當然也有自己的一套。在練習Hello World程式時我們也稍微了解到,Rust用的println!不是一般的函數,而是巨集!

巨集println!第一個參數傳入字串的樣本,並允許在字串樣本中使用大括號{}來表示要預留的空位,格式化時就會把緊接著傳入進巨集println!的其它參數用來取代字串樣本預留的空位。

大括號{}內還可以再加上其他訊息,用來控制參數代入的順序和方式,這個在之後的章節會更詳細地介紹,現在我們只需知道我們可以直接把變數名稱填寫到大括號{}內,就不必再從後面的參數傳入空位的值了。程式如下:

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!的敘述,在螢幕上印出訊息,來提示使用者該要開始輸入要猜的數字了。程式如下:

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

再來,我們要宣告出一個變數,用來儲存使用者輸入的文字。程式如下:

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

程式第12行,由於我們不知道使用者究竟會輸入怎麼樣的文字,因此在宣告guess變數時加上mut關鍵字。String為Rust程式語言的標準函式庫所內建的一個結構體,使用UTF-8編碼來處理可變動長度的字串資料,其提供的new函數可以快速產生String結構的實體。

接著,為了讓使用者輸入文字了,我們需要使用std::io這個模組(mod),std::io提供了一些有關輸入與輸出的功能,在之後的章節會詳細介紹模組的用法。

若要在程式碼中方便使用std::io模組,我們可以利用use關鍵字將其引用進來。程式如下:

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來表示std::io模組了!

在預設的情況下,Rust只會自動引用一些東西進入目前程式的使用範圍(scope),這些自動引入的東西均定義在std::prelude模組中。如果要使用沒有定義在std::prelude模組中的東西,就要靠use關鍵字來引用。

引用了std::io模組之後,即可加入新的程式敘述:

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

程式第16行,利用std::io提供的stdin函數,來建立出一個用來處理標準輸入的Stdin結構實體。第17行呼叫其提供的read_line方法,來讓使用者能透過標準輸入串流輸入文字,並且將輸入的文字存進從參數傳入的String結構實體。前面我們使用use關鍵字,將std::io模組加入至我們的程式scope中,如果只是臨時要使用某套件的某個模組或函數,不想將其加入整個程式的scope中的話,也可以省略use關鍵字,之後在使用時改用完整的名稱來指定即可。舉例來說,不用use關鍵字來引用std::io模組的話,原本第16行的io::stdin()可以改寫成std::io::stdin()

傳入read_line方法的參數型別為&mut String&所代表的意思是取得這個變數的參考(reference),至於為什麼在此要用參考的方式傳遞參數,將留在之後的章節介紹。在&後面加上mut是用來表示這個參考所指到的值是可變的,因為我們需要把使用者輸入的文字存進傳進read_line方法的String結構中,所以使用mutread_line方法還會回傳一個Result列舉的值,方便我們判斷這個方法有沒有執行成功。事實上,我們不一定要處理函數或是方法回傳的Result列舉的值,只不過若沒有去使用到函數或是方法的回傳值的話,編譯器是會出現警告訊息的!為了能夠讓增加程式的安全性,我們還是應該要去處理函數或是方法的回傳值。

Result列舉是Rust程式語言提供的型別,只有兩種值(準確來說是「變體」(variant)),分別是OkErr。顧名思義,Ok代表程式執行成功,Err則代表程式執行失敗,這兩種值分別包裹執行成功的值和執行錯誤的值。Result列舉經常被函數或是方法作為回傳值,來回傳函數或是方法有沒有執行成功。由於判斷「Result列舉的值是Ok還是Err」的這個動作會很頻繁的在程式碼中被使用到,因此Rust程式語言提供了一個簡化判斷Result列舉的語法糖──expect方法。

程式第18行,我們呼叫了read_line方法所回傳的Result列舉的expect方法,如果該Result列舉的值是Err的話,將會造成程式panic(驚慌),而panic訊息就是代入至expect方法之參數的Failed to read line文字。如果該Result列舉的值是Ok的話,則會回傳被Ok包裹的值。

接著使用println!巨集,輸出使用者剛才輸入的文字。程式如下:

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

我們剛才使用read_line方法讓使用者輸入進來的文字是String型別,為了讓它能夠與猜數字的答案secret_number進行數值大小比較,我們必須將String轉成i32。Rust程式語言對於數值型別,區分了有號整數(i)、無號整數(u)和浮點數(f),整數依照表示範圍區分為8位元、16位元、32位元、64位元和128位元,浮點數也依照精準度區分為32位元和64位元。由於我們在使用gen_range方法時所代入的參數值為整數的範圍,在Rust程式語言中,直接寫在程式碼的整數預設使用32位元的整數型別,也就是i32,因此secret_number的型別也就是i32。為了能夠與型別為i32secret_number變數進行比較,我們需要將guess變數所儲存的String結構體也轉成i32型別。程式如下:

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

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

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

程式第20行,我們使用let關鍵字宣告了一個guess變數,並且使用:語法將其的型別指定為i32。可是程式第15行不是已經有用let宣告過guess變數了嗎?而且還使用了mut關鍵字讓它是可變得,為什麼不直接使用這個guess變數,而在程式第20行的時候用let重新宣告呢?這是因為Rust程式語言所使用的變數一旦被定義好型別,就不能再進行改變了,程式第20行其實是使用let宣告一個新的且型別不同的guess變數,由於變數名稱相同的關係,這個新的guess變數在宣告之後會遮蔽(shadowing)掉原先的guess變數。程式=右邊的式子會先執行完畢之後再指派給左邊,因此程式第20行等號右邊的guess變數,其實還是第14行宣告的那個String結構體的guess變數。String結構實體提供了trim方法,可以消除字串頭尾所使用的空白與換行字元,並回傳另一種型別的字串──str,這個在之後的章節會詳細介紹。總而言之,透過trim方法,我們也可以過濾掉使用者確認輸入時按下Enter鍵而產生的\n字元,避免造成之後轉換成數值型別時發生問題。無論是String還是str型別的字串,都有實作FromStr這個特性,因此可以使用parse方法,來將字串轉成數值。在此,變數的型別不像程式第14行一樣,可以自動根據最一開始指派的值來定義型別,而是必須先定義好,否則會編譯錯誤。或者,也可以用「turbofish」語法來指定parse方法要轉成的數值型別,語法如下:

let guess = guess.trim().parse::<i32>();

或許這種::<>的語法很像是一種叫作「Turbot」(大口鰜)的魚,所以才會稱作「turbofish」吧?之後的章節會詳細介紹<>語法所表示的泛型(generic)概念。

程式第20行,由於parse方法執行之後會回傳Result列舉,所以同樣也可以使用expect方法來處理。

接著就要來判斷使用者輸入的數值是否剛好猜中答案,如果是的話,要印出You win!;如果不是的話,要印出Too small!或是Too big!。程式如下:

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

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

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

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

程式第24行到第28行,使用了match關鍵字來控制程式流程。match關鍵字是Rust程式語言中提供的一種類似其它語言switch關鍵字控制流程的功能,它能夠判斷傳入的數值符合哪個它定義好的「arm」(手臂),來決定要執行什麼程式。型別為i32guess變數,可以使用cmp這個方法來與傳入另一個i32的值進行比較,如果傳進來比較的值小於guess變數的值,cmp方法就會回傳std::cmp::Ordering列舉的Less值;如果大於,會回傳Greater;如果等於,會回傳Equal。程式第25行定義了一個match的arm,如果傳入match的值為std::cmp::Ordering列舉的Less值的話,程式就會用println!巨集印出Too small!。同理,程式第26、27行就是定義符合其它值時候的流程。之後的章節會詳細介紹match的用法,現在先理解到這樣的程度即可。

由於std::cmp::Ordering::Less這樣的寫法實在又臭又長,我們可以利用use關鍵字,將std::cmp::Ordering引用到我們程式的scope中。程式碼可以改寫成如下的樣子:

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

    let mut guess = String::new();

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

    let guess: i32 = 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::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: i32 = 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!"),
        }
    }
}

程式第13行和第31行,我們利用loop關鍵字將讓使用者輸入文字並顯示猜測結果的程式包成了一個區塊,程式如果未在這個區塊執行到break或是return敘述,就會不斷的重複執行這個區塊內的敘述。為了能夠讓使用者在猜對數字之後結束程式,我們需要再將程式進行改寫,如下:

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: i32 = 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!");
                break;
            }
        }
    }
}

程式第29行到第32行,改寫了原先Ordering::Equal這個match arm的寫法,在印出You win!文字之後執行break敘述。和其他大多數的程式語言一樣,Rust程式語言loop區塊中的break敘述是用來跳出迴圈;continue敘述是用來跳過這次迴圈的執行,直接進入下一次。由於原先的arm只有一行敘述,所以不需要另外用大括號{}表示程式區塊,但現在需要執行超過一個敘述,因此要使用大括號{}將需要執行的敘述包起來。

至此,我們已經差不多完成了這個猜數字程式了。可以用cargo run指令玩個幾次試試。

rust-guess-number

我們預期使用者輸入的字串是數字,接著再由我們的程式將字串轉成數值型別,但如果使用者輸入的字串不是正確的數字,就會出現帶有Please type a number!訊息的panic,程式就這樣莫名其妙的停止結束執行了。

rust-guess-number

這樣的方式對於使用者來說會是一個很糟糕的體驗,為了解決這個問題,我們可以利用match關鍵字來處理parse方法回傳的Result列舉。將程式改寫如下:

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: i32 = 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;
            }
        }
    }
}

程式第22行到第25行,我們把原先使用expect方法來處理Result列舉的方式改為使用match關鍵字。在match的arm中,我們判斷parse方法回傳的Result是否符合Ok(num)或是Err(_)num是一個表示Ok第一個包裹的參數的變數,透過這樣的語法,就可以在=>右方用num取得Ok包裹在第一個參數的值。若在此使用的不是num這樣的變數名稱,而是一個明確的值的話,就表示parse方法回傳的Result::Ok,第一個參數必須要正好包裹著這個明確的值。若我們將程式改寫成如下的樣子:

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: i32 = match guess.trim().parse() {
            Ok(-1) => {
                println!("Bye bye!");
                break;
            }
            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;
            }
        }
    }
}

當使用者猜的數字為-1時,就會印出Bye bye!文字,然後跳出迴圈,結束程式。

rust-guess-number

請注意,若把Ok(-1)這條arm放在Ok(num)之後,將會永遠無法被執行到,因為match關鍵字判斷要執行哪條arm的方式是依照程式碼撰寫順序的。

至於Err(_)_,則是表示要忽略這個參數,如果將其改寫成Err(e)的話,程式雖然也可以被編譯執行,但因e這個變數並沒有用到,所以編譯器會出現警告訊息。

程式寫到這裡,這個章節的猜數字程式已經完成了,最後使用cargo build --release指令將專案編譯出執行檔,然後把這個執行檔傳給親朋好友們玩玩看吧!

rust-guess-number

總結

在這一章中,我們成功地完成了猜數字程式,已經用過了letmatchuseextern crate等等的關鍵字,稍微接觸了Rust的基本資料型別、結構、特性和列舉,也用了一些標準函式庫提供的功能。在接下來的章節中,將會對變數、資料型別、函數、程式流程進行更深入的探討。

下一章:Rust程式語言的基礎概念