如何使用Rust寫個猜數(shù)字游戲
開頭
讓我們一起動手完成一個項目,來快速上手 Rust!本章將介紹 Rust 中一些常用概念,并通過真實的程序來展示如何運用它們。
將會學到 let、match、方法(method)、關聯(lián)函數(shù)(associated function)、外部 crate 等知識!
后續(xù)文章會深入探討這些概念的細節(jié)。
在這篇章,我們將練習基礎內容。
我們會實現(xiàn)一個經(jīng)典的新手編程問題:猜猜看游戲。
它是這么工作的:程序將會隨機生成一個 1 到 100 之間的隨機整數(shù)。接著它會請玩家猜一個數(shù)并輸入,然后提示猜測是大了還是小了。如果猜對了,它會打印祝賀信息并退出。
準備一個新項目
創(chuàng)建的 projects 目錄,使用 Cargo 新建一個項目,如下:
cargo new guessing_game cd guessing_game
現(xiàn)在使用 cargo run 命令,一步完成 “Hello, world!” 程序的編譯和運行:
當你需要在項目中快速迭代時,run 命令就能派上用場,正如我們在這個游戲項目中做的,在下一次迭代之前快速測試每一次迭代。
打開 src/main.rs 文件。我們將會在這個文件中編寫全部的代碼。
處理一次猜測
猜猜看程序的第一部分請求和處理用戶輸入,并檢查輸入是否符合預期的格式。首先,允許玩家輸入猜測。在 src/main.rs 中輸入代碼。
use std::io; fn main() { println!("Guess the number!"); println!("Please input your guess."); let mut guess = String::new(); //接收用戶輸入,并將其存入guess變量中 io::stdin() .read_line(&mut guess ) .expect("Failed to read line"); //打印用戶輸入 println!("You guessed: {}", guess); }
運行代碼 cargo run
然后在控制臺中輸入, 最后結果如下:
至此為止,游戲的第一部分已經(jīng)完成:我們從鍵盤獲取輸入并打印了出來。
這些代碼包含很多信息,我們把陌生的內容講解一下:
為了獲取用戶輸入并打印結果作為輸出,我們需要將 io輸入/輸出庫引入當前作用域。io 庫來自于標準庫,也被稱為 std:
use std::io;
默認情況下,Rust 設定了若干個會自動導入到每個程序作用域中的標準庫內容,這組內容被稱為 預導入(preclude) 內容。你可以在 標準庫文檔
中查看預導入的所有內容。
如果你需要的類型不在預導入內容中,就必須使用 use 語句顯式地將其引入作用域。std::io 庫提供很多有用的功能,包括接收用戶輸入的功能。
創(chuàng)建一個 變量(variable)來儲存用戶輸入,像這樣:
let mut guess = String::new();
現(xiàn)在程序開始變得有意思了!這一小行代碼發(fā)生了很多事。
我們使用 let 語句來創(chuàng)建變量。這里是另外一個例子:
let apples = 5;
這行代碼新建了一個叫做 apples 的變量并把它綁定到值 5 上。
在 Rust 中,變量默認是不可變的,這意味著一旦我們給變量賦值,這個值就不再可以修改了。我們將會在后面文章中講解 “變量與可變性” 這個概念。
下面的例子展示了如何在變量名前使用 mut 來使一個變量可變:
let apples = 5; // 不可變 let mut bananas = 5; // 可變
注意:// 語法開始一個注釋,持續(xù)到行尾。Rust 忽略注釋中的所有內容,后面文章中將會詳細介紹注釋。
回到猜猜看程序中?,F(xiàn)在我們知道了 let mut guess 會引入一個叫做 guess 的可變變量。等號(=)告訴 Rust 我們現(xiàn)在想將某個值綁定在變量上。等號的右邊是 guess 所綁定的值,它是 String::new 的結果,這個函數(shù)會返回一個 String 的新實例。String 是一個標準庫提供的字符串類型,它是 UTF-8 編碼的可增長文本塊。
::new 那一行的 :: 語法表明 new 是 String 類型的一個 關聯(lián)函數(shù)(associated function)。關聯(lián)函數(shù)是針對類型實現(xiàn)的,在這個例子中是 String,而不是 String 的某個特定實例。一些語言中把它稱為 靜態(tài)方法(static method)。
new 函數(shù)創(chuàng)建了一個新的空字符串,你會發(fā)現(xiàn)很多類型上有 new 函數(shù),因為它是創(chuàng)建類型實例的慣用函數(shù)名。
總的來說,let mut guess = String::new(); 這一行創(chuàng)建了一個可變變量,當前它綁定到一個新的 String 空實例上。
回憶一下,我們在程序的第一行使用 use std::io; 從標準庫中引入了輸入/輸出功能?,F(xiàn)在調用 io 庫中的函數(shù) stdin:
io::stdin() .read_line(&mut guess)
如果程序的開頭沒有使用 use std::io; 引入 io 庫,我們仍可以通過把函數(shù)調用寫成 std::io::stdin 來使用函數(shù)。
stdin 函數(shù)返回一個 std::io::Stdin 的實例,這代表終端標準輸入句柄的類型。
代碼的下一部分,.read_line(&mut guess),調用 read_line 方法從標準輸入句柄獲取用戶輸入。
我們還將 &mut guess 作為參數(shù)傳遞給 read_line() 函數(shù),讓其將用戶輸入儲存到這個字符串中。
read_line 的工作是,無論用戶在標準輸入中鍵入什么內容,都將其追加(不會覆蓋其原有內容)到一個字符串中,因此它需要字符串作為參數(shù)。這個字符串參數(shù)應該是可變的,以便 read_line 將用戶輸入附加上去。
& 表示這個參數(shù)是一個 引用(reference),它允許多處代碼訪問同一處數(shù)據(jù),而無需在內存中多次拷貝。引用是一個復雜的特性,Rust 的一個主要優(yōu)勢就是安全而簡單的操縱引用。
現(xiàn)在,我們只需知道它像變量一樣,默認是不可變的。因此,需要寫成 &mut guess 來使其可變,而不是 &guess。(第后面文章會更全面的解釋引用。)
我們還沒有完全分析完這行代碼因為第 8 行代碼換行了,但要注意:它仍是邏輯行(雖然換行了但仍是語句)的一部分。 我們也可以將代碼這樣寫:
io::stdin().read_line(&mut guess).expect("Failed to read line");
不過,過長的代碼行難以閱讀,所以最好拆開來寫。通常來說,當使用 .method_name() 語法調用方法時引入換行符和空格將長的代碼行拆開是明智的。現(xiàn)在來看看這行代碼干了什么。
之前提到了 read_line 會將用戶輸入附加到傳遞給它的字符串中,不過它也會返回一個類型為 Result 的值。 Result 是一種枚舉類型,通常也寫作 enum。枚舉類型變量的值可以是多種可能狀態(tài)中的一個。我們把每種可能的狀態(tài)稱為一種 枚舉成員(variant)。
后面文章將介紹枚舉的更多細節(jié)。這里的 Result 類型將用來編碼錯誤處理的信息。
Result 的成員是 Ok 和 Err,Ok 成員表示操作成功,內部包含成功時產(chǎn)生的值。Err 成員則意味著操作失敗,并且包含失敗的前因后果。
這些 Result 類型的作用是編碼錯誤處理信息。Result 類型的值,像其他類型一樣,擁有定義于其上的方法。Result 的實例擁有 expect方法。
如果 io::Result 實例的值是 Err,expect 會導致程序崩潰,并顯示當做參數(shù)傳遞給 expect 的信息。如果 read_line 方法返回 Err,則可能是來源于底層操作系統(tǒng)錯誤的結果。如果 Result 實例的值是 Ok,expect 會獲取 Ok 中的值并原樣返回。在本例中,這個值是用戶輸入到標準輸入中的字節(jié)數(shù)。
如果不調用 expect,程序也能編譯,不過會出現(xiàn)一個警告:
Rust 警告我們沒有使用 read_line 的返回值 Result,說明有一個可能的錯誤沒有處理。
消除警告的正確做法是實際去編寫錯誤處理代碼,不過由于我們就是希望程序在出現(xiàn)問題時立即崩潰,所以直接使用 expect。后面文章中, 會學習如何從錯誤中恢復。
除了位于結尾的右花括號,目前為止就只有這一行代碼值得討論一下了,就是這一行:
println!("You guessed: {guess}");
這行代碼現(xiàn)在打印了存儲用戶輸入的字符串。里面的 {} 是預留在特定位置的占位符, 當打印變量的值時,變量名可以寫進大括號中。
當打印表達式的執(zhí)行結果時,格式化字符串(format string)中大括號中留空,格式化字符串后跟逗號分隔的需要打印的表達式列表,其順序與每一個空大括號占位符的順序一致。
在一個 println! 調用中打印變量和表達式的值看起來像這樣:
let x = 5; let y = 10; println!("x = {x} and y + 2 = {}", y + 2);
這行代碼會打印出 x = 5 and y + 2 = 12。
生成一個秘密數(shù)字
接下來,需要生成一個秘密數(shù)字,好讓用戶來猜。秘密數(shù)字應該每次都不同,這樣重復玩才不會乏味;范圍應該在 1 到 100 之間,這樣才不會太困難。Rust 標準庫中尚未包含隨機數(shù)功能。然而,Rust 團隊還是提供了一個包含上述功能的 randcrate。
使用 crate 來增加更多功能
記住,crate 是一個 Rust 代碼包。我們正在構建的項目是一個 二進制 crate,它生成一個可執(zhí)行文件。randcrate
是一個 庫 crate,庫 crate 可以包含任意能被其他程序使用的代碼,但是不能自執(zhí)行。
Cargo 對外部 crate 的運用是其真正的亮點所在。在我們使用 rand 編寫代碼之前,需要修改 Cargo.toml 文件,引入一個 rand 依賴?,F(xiàn)在打開這個文件并將下面這一行添加到 [dependencies] 片段標題之下。在當前版本下,請確保按照我們這里的方式指定 rand,否則本教程中的示例代碼可能無法工作。
文件名:Cargo.toml
[dependencies] rand = "0.8.5"
在 Cargo.toml 文件中,標題以及之后的內容屬同一個片段,直到遇到下一個標題才開始新的片段。[dependencies] 片段告訴 Cargo 本項目依賴了哪些外部 crate 及其版本。本例中,我們使用語義化版本 0.8.5 來指定 rand crate。Cargo 理解 語義化版本(Semantic Versioning)(有時也稱為 SemVer),這是一種定義版本號的標準。0.8.5 事實上是 ^0.8.5 的簡寫,它表示任何至少是 0.8.5 但小于 0.9.0 的版本。
Cargo 認為這些版本與 0.8.5 版本的公有 API 相兼容,這樣的版本指定確保了我們可以獲取能使本章代碼編譯的最新的補?。╬atch)版本。任何大于 0.8.5
的版本不能保證和接下來的示例采用了相同的 API?,F(xiàn)在,不修改任何代碼,構建項目,如:cargo build
$ cargo build Updating crates.io index Downloaded rand v0.8.5 Downloaded libc v0.2.127 Downloaded getrandom v0.2.7 Downloaded cfg-if v1.0.0 Downloaded ppv-lite86 v0.2.16 Downloaded rand_chacha v0.3.1 Downloaded rand_core v0.6.3 Compiling libc v0.2.127 Compiling getrandom v0.2.7 Compiling cfg-if v1.0.0 Compiling ppv-lite86 v0.2.16 Compiling rand_core v0.6.3 Compiling rand_chacha v0.3.1 Compiling rand v0.8.5 Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 2.53s
可能會出現(xiàn)不同的版本號(多虧了語義化版本,它們與代碼是兼容的?。瑫r顯示順序也可能會有所不同。
現(xiàn)在我們有了一個外部依賴,Cargo 從 registry 上獲取所有包的最新版本信息,這是一份來自 Crates.io 的數(shù)據(jù)拷貝。
Crates.io 是 Rust 生態(tài)環(huán)境中的開發(fā)者們向他人貢獻 Rust 開源項目的地方。
在更新完 registry 后,Cargo 檢查 [dependencies] 片段并下載列表中包含但還未下載的 crates。本例中,雖然只聲明了 rand 一個依賴,然而 Cargo 還是額外獲取了 rand 所需要的其他 crates,因為 rand 依賴它們來正常工作。下載完成后,Rust 編譯依賴,然后使用這些依賴編譯項目。
Cargo.lock文件確保構建是可重現(xiàn)的
Cargo 有一個機制來確保任何人在任何時候重新構建代碼,都會產(chǎn)生相同的結果:Cargo 只會使用你指定的依賴版本,除非你又手動指定了別的。例如,如果下周 rand crate 的 0.8.6 版本出來了,它修復了一個重要的 bug,同時也含有一個會破壞代碼運行的缺陷。為了處理這個問題,Rust 在你第一次運行 cargo build 時建立了 Cargo.lock 文件,我們現(xiàn)在可以在_guessing_game_ 目錄找到它。
當?shù)谝淮螛嫿椖繒r,Cargo 計算出所有符合要求的依賴版本并寫入 Cargo.lock 文件。
當將來其他人構建項目時,Cargo 會發(fā)現(xiàn) Cargo.lock 已存在并使用其中指定的版本,而不是再次計算所有的版本。這使得你擁有了一個自動化的可重現(xiàn)的構建。
換句話說,項目會持續(xù)使用 0.8.5 直到你顯式升級,多虧有了 Cargo.lock 文件。由于 Cargo.lock 文件對于“可重復構建”非常重要,因此它通常會和項目中的其余代碼一樣納入到版本控制系統(tǒng)中。
更新 crate 到一個新版本
當你 確實 需要升級 crate 時,Cargo 提供了這樣一個命令,update,它會忽略 Cargo.lock 文件,并計算出所有符合 Cargo.toml 聲明的最新版本。Cargo 接下來會把這些版本寫入 Cargo.lock 文件。不過,Cargo 默認只會尋找大于 0.8.5 而小于 0.9.0 的版本。如果 rand crate 發(fā)布了兩個新版本,0.8.6 和 0.9.0,在運行 cargo update 時會出現(xiàn)如下內容:
$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6
Cargo 忽略了 0.9.0 版本。這時,你也會注意到的 Cargo.lock 文件中的變化無外乎現(xiàn)在使用的 rand crate 版本是0.8.6 。如果想要使用 0.9.0 版本的 rand 或是任何 0.9.x 系列的版本,必須像這樣更新 Cargo.toml 文件:
[dependencies] rand = "0.9.0"
下一次運行 cargo build 時,Cargo 會從 registry 更新可用的 crate,并根據(jù)你指定的新版本重新計算。
后面文章會講到 Cargo 及其生態(tài)系統(tǒng)的更多內容,不過目前你只需要了解這么多。通過 Cargo 復用庫文件非常容易,因此 Rustacean 能夠編寫出由很多包組裝而成的更輕巧的項目。
讓我們開始使用 rand 來生成一個猜猜看隨機數(shù)。下一步是更新 src/main.rs
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}"); }
首先,我們新增了一行 use rand::Rng;。Rng 是一個 trait,它定義了隨機數(shù)生成器應實現(xiàn)的方法,想使用這些方法的話,此 trait 必須在作用域中。后面文章會詳細介紹 trait。
接下來,我們在中間還新增加了兩行。第一行調用了 rand::thread_rng 函數(shù)提供實際使用的隨機數(shù)生成器:它位于當前執(zhí)行線程的本地環(huán)境中,并從操作系統(tǒng)獲取 seed。
接著調用隨機數(shù)生成器的 gen_range 方法。這個方法由 use rand::Rng 語句引入到作用域的 Rng trait 定義。gen_range 方法獲取一個范圍表達式(range expression)作為參數(shù),并生成一個在此范圍之間的隨機數(shù)。
這里使用的這類范圍表達式使用了 start…=end 這樣的形式,也就是說包含了上下端點,所以需要指定 1…=100 來請求一個 1 和 100 之間的數(shù)。
注意:你不可能憑空就知道應該 use 哪個 trait 以及該從 crate 中調用哪個方法,因此每個 crate 有使用說明文檔。
Cargo 有一個很棒的功能是:在當前項目根目錄下運行 cargo doc --open 命令來構建所有本地依賴提供的文檔,并在瀏覽器中打開。
例如,假設你對 rand crate 中的其他功能感興趣,你可以運行 cargo doc --open 并點擊左側導航欄中的 rand。
新增加的第 9 行代碼打印出了秘密數(shù)字。這在開發(fā)程序時很有用,因為可以測試它,不過在最終版本中會刪掉它。如果游戲一開始就打印出結果就沒什么可玩的了!
嘗試運行程序幾次: 你應該能得到不同的隨機數(shù),同時它們應該都是在 1 和 100 之間的。
比較猜測的數(shù)字和秘密數(shù)字
現(xiàn)在有了用戶輸入和一個隨機數(shù),我們可以比較它們。這個步驟如示例 2-4 所示。注意這段代碼還不能通過編譯,我們稍后會解釋。
文件名:src/main.rs
use rand::Rng; use std::cmp::Ordering; use std::io; 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(); //接收用戶輸入,并將其存入guess變量中 io::stdin().read_line(&mut guess ).expect("Failed to read line"); //打印用戶輸入 println!("You guessed: {}", guess); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } }
首先我們增加了另一個 use 聲明,從標準庫引入了一個叫做 std::cmp::Ordering 的類型到作用域中。 Ordering 也是一個枚舉,不過它的成員是 Less、Greater 和 Equal。這是比較兩個值時可能出現(xiàn)的三種結果。
接著,底部的五行新代碼使用了 Ordering 類型,cmp 方法用來比較兩個值并可以在任何可比較的值上調用。
它獲取一個被比較值的引用:這里是把 guess 與 secret_number 做比較。然后它會返回一個剛才通過 use 引入作用域的 Ordering 枚舉的成員。使用一個 match 表達式,根據(jù)對 guess 和 secret_number 調用 cmp 返回的 Ordering 成員來決定接下來做什么。
一個 match 表達式由 分支(arms) 構成。一個分支包含一個 模式(pattern)和表達式開頭的值與分支模式相匹配時應該執(zhí)行的代碼。Rust 獲取提供給 match 的值并挨個檢查每個分支的模式。match 結構和模式是 Rust 中強大的功能,它體現(xiàn)了代碼可能遇到的多種情形,并幫助你確保沒有遺漏處理。這些功能將分別在后面文章中介紹。
讓我們看看使用 match 表達式的例子。假設用戶猜了 50,這時隨機生成的秘密數(shù)字是 38。
比較 50 與 38 時,因為 50 比 38 要大,cmp 方法會返回 Ordering::Greater。Ordering::Greater 是 match 表達式得到的值。它檢查第一個分支的模式,Ordering::Less 與 Ordering::Greater并不匹配,所以它忽略了這個分支的代碼并來到下一個分支。下一個分支的模式是 Ordering::Greater,正確 匹配!
這個分支關聯(lián)的代碼被執(zhí)行,在屏幕打印出 Too big!。match 表達式會在第一次成功匹配后終止,因為該場景下沒有檢查最后一個分支的必要。
我們來看看上面代碼為什不能執(zhí)行的原因
_ types_)。Rust 有一個靜態(tài)強類型系統(tǒng),同時也有類型推斷。當我們寫出 let guess = String::new() 時,Rust 推斷出 guess 應該是 String 類型, 并不需要我們寫出類型。
另一方面,secret_number,是數(shù)字類型。幾個數(shù)字類型擁有 1 到 100 之間的值:32 位數(shù)字 i32;32 位無符號數(shù)字 u32;64 位數(shù)字 i64 等等。Rust 默認使用 i32,所以它是 secret_number 的類型,除非增加類型信息,或任何能讓 Rust 推斷出不同數(shù)值類型的信息。這里錯誤的原因在于 Rust 不會比較字符串類型和數(shù)字類型。
所以我們必須把從輸入中讀取到的 String 轉換為一個真正的數(shù)字類型,才好與秘密數(shù)字進行比較。這可以通過在 main 函數(shù)體中增加如下代碼來實現(xiàn):
use rand::Rng; use std::cmp::Ordering; use std::io; 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(); //接收用戶輸入,并將其存入guess變量中 io::stdin().read_line(&mut guess ).expect("Failed to read line"); //將用戶輸入的字符串轉換為數(shù)字 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!"), } }
這行新代碼是 15 行
這里創(chuàng)建了一個叫做 guess 的變量。不過等等,不是已經(jīng)有了一個叫做 guess 的變量了嗎?確實如此,不過 Rust 允許用一個新值來 隱藏 (Shadowing) guess 之前的值。
這個功能常用在需要轉換值類型之類的場景。它允許我們復用 guess 變量的名字,而不是被迫創(chuàng)建兩個不同變量,諸如 guess_str 和 guess 之類。后面文章會介紹 shadowing 的更多細節(jié),現(xiàn)在只需知道這個功能經(jīng)常用于將一個類型的值轉換為另一個類型的值。
我們將這個新變量綁定到 guess.trim().parse() 表達式上。表達式中的 guess 指的是包含輸入的字符串類型 guess 變量。String 實例的 trim 方法會去除字符串開頭和結尾的空白字符,我們必須執(zhí)行此方法才能將字符串與 u32 比較,因為 u32 只能包含數(shù)值型數(shù)據(jù)。
用戶必須輸入 enter 鍵才能讓 read_line 返回并輸入他們的猜想,這將會在字符串中增加一個換行(newline)符。例如,用戶輸入 5 并按下 enter(在 Windows 上,按下 enter 鍵會得到一個回車符和一個換行符,\r\n),guess 看起來像這樣:5\n 或者 5\r\n。\n 代表 “換行”,回車鍵;\r 代表 “回車”,回車鍵。trim 方法會消除 \n 或者 \r\n,只留下 5。
字符串的parse方法 將字符串轉換成其他類型。這里用它來把字符串轉換為數(shù)值。我們需要告訴 Rust 具體的數(shù)字類型,這里通過 let guess: u32 指定。guess 后面的冒號(:)告訴 Rust 我們指定了變量的類型。Rust 有一些內建的數(shù)字類型;u32 是一個無符號的 32 位整型。對于不大的正整數(shù)來說,它是不錯的默認類型,第后面文章還會講到其他數(shù)字類型。
另外,程序中的 u32 注解以及與 secret_number 的比較,意味著 Rust 會推斷出 secret_number 也是 u32 類型?,F(xiàn)在可以使用相同類型比較兩個值了!
parse 方法只有在字符邏輯上可以轉換為數(shù)字的時候才能工作所以非常容易出錯。
例如,字符串中包含 A??%
,就無法將其轉換為一個數(shù)字。因此,parse 方法返回一個 Result 類型。像之前 “使用Result類型來處理潛在的錯誤” 討論的 read_line 方法那樣,再次按部就班的用 expect 方法處理即可。
如果 parse 不能從字符串生成一個數(shù)字,返回一個 Result 的 Err 成員時,expect 會使游戲崩潰并打印附帶的信息。如果 parse 成功地將字符串轉換為一個數(shù)字,它會返回 Result 的 Ok 成員,然后 expect 會返回 Ok 值中的數(shù)字。
現(xiàn)在讓我們運行程序!: cargo run
漂亮!即便是在猜測之前添加了空格,程序依然能判斷出用戶猜測了 76。多運行程序幾次,輸入不同的數(shù)字來檢驗不同的行為:猜一個正確的數(shù)字,猜一個過大的數(shù)字和猜一個過小的數(shù)字。
現(xiàn)在游戲已經(jīng)大體上能玩了,不過用戶只能猜一次。我們可以增加一個循環(huán)來改變它吧!
使用循環(huán)來允許多次猜測
loop 關鍵字創(chuàng)建了一個無限循環(huán)。我們會增加循環(huán)來給用戶更多機會猜數(shù)字:
use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..=100); println!("The secret number is: {secret_number}"); loop {//無限循環(huán) println!("Please input your guess."); let mut guess = String::new(); //接收用戶輸入,并將其存入guess變量中 io::stdin().read_line(&mut guess ).expect("Failed to read line"); //將用戶輸入的字符串轉換為數(shù)字 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!"), } } }
如上所示,我們將提示用戶猜測之后的所有內容移動到了循環(huán)中。確保 loop 循環(huán)中的代碼多縮進四個空格,再次運行程序。注意這里有一個新問題,因為程序忠實地執(zhí)行了我們的要求:永遠地請求另一個猜測,用戶好像無法退出??!
用戶總能使用 ctrl-c 終止程序。不過還有另一個方法跳出無限循環(huán),就是 “比較猜測與秘密數(shù)字” 部分提到的 parse:如果用戶輸入的答案不是一個數(shù)字,程序會崩潰。我們可以利用這一點來退出,輸入 q將會退出程序,同時你會注意到其他任何非數(shù)字輸入也一樣。然而,這并不理想,我們想要當猜測正確的數(shù)字時游戲停止。
猜測正確后退出
讓我們增加一個 break 語句,在用戶猜對時退出游戲:
use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..=100); println!("The secret number is: {secret_number}"); loop { //無限循環(huán) println!("Please input your guess."); let mut guess = String::new(); //接收用戶輸入,并將其存入guess變量中 io::stdin().read_line(&mut guess ).expect("Failed to read line"); //將用戶輸入的字符串轉換為數(shù)字 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 => { //匹配到相等時,退出循環(huán) println!("You win!"); break; } } } }
通過在 You win! 之后增加一行 break,用戶猜對了神秘數(shù)字后會退出循環(huán)。退出循環(huán)也意味著退出程序,因為循環(huán)是 main 的最后一部分。
處理無效輸入
為了進一步改善游戲性,不要在用戶輸入非數(shù)字時崩潰,需要忽略非數(shù)字,讓用戶可以繼續(xù)猜測??梢酝ㄟ^修改 guess 將 String 轉化為 u32 那部分代碼來實現(xiàn),如下所示:
use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..=100); println!("The secret number is: {secret_number}"); loop { //無限循環(huán) println!("Please input your guess."); let mut guess = String::new(); //接收用戶輸入,并將其存入guess變量中 io::stdin().read_line(&mut guess ).expect("Failed to read line"); //將用戶輸入的字符串轉換為數(shù)字 let guess: u32 = match guess.trim().parse() { Ok(num) => num, //如果轉換成功,返回數(shù)字 Err(_) => continue, //如果轉換失敗,繼續(xù)循環(huán) }; //打印用戶輸入 println!("You guessed: {}", guess); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { //匹配到相等時,退出循環(huán) println!("You win!"); break; } } } }
我們將 expect 調用換成 match 語句,以從遇到錯誤就崩潰轉換為處理錯誤。須知 parse 返回一個 Result 類型,而 Result 是一個擁有 Ok 或 Err 成員的枚舉。這里使用的 match 表達式,和之前處理 cmp 方法返回 Ordering 時用的一樣。
如果 parse 能夠成功的將字符串轉換為一個數(shù)字,它會返回一個包含結果數(shù)字的 Ok。這個 Ok 值與 match 第一個分支的模式相匹配,該分支對應的動作返回 Ok 值中的數(shù)字 num,最后如愿變成新創(chuàng)建的 guess 變量。
如果 parse 不能將字符串轉換為一個數(shù)字,它會返回一個包含更多錯誤信息的 Err。Err 值不能匹配第一個 match 分支的 Ok(num) 模式,但是會匹配第二個分支的 Err() 模式: 是一個通配符值,本例中用來匹配所有 Err 值,不管其中有何種信息。
所以程序會執(zhí)行第二個分支的動作,continue 意味著進入 loop 的下一次循環(huán),請求另一個猜測。這樣程序就有效的忽略了 parse 可能遇到的所有錯誤!
現(xiàn)在萬事俱備,只需運行 cargo run:
太棒了!再有最后一個小的修改,就能完成猜猜看游戲了:還記得程序依然會打印出秘密數(shù)字。在測試時還好,但正式發(fā)布時會毀了游戲。刪掉打印秘密數(shù)字的 println!。
println!("The secret number is: {secret_number}");
此時此刻,你順利完成了猜猜看游戲。恭喜!, 下面是最終的代碼
use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..=100); loop { //無限循環(huán) println!("Please input your guess."); let mut guess = String::new(); //接收用戶輸入,并將其存入guess變量中 io::stdin().read_line(&mut guess ).expect("Failed to read line"); //將用戶輸入的字符串轉換為數(shù)字 let guess: u32 = match guess.trim().parse() { Ok(num) => num, //如果轉換成功,返回數(shù)字 Err(_) => continue, //如果轉換失敗,繼續(xù)循環(huán) }; //打印用戶輸入 println!("You guessed: {}", guess); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { //匹配到相等時,退出循環(huán) println!("You win!"); break; } } } }
總結
本項目通過動手實踐,向你介紹了 Rust 新概念:let、match、函數(shù)、使用外部 crate 等等,接下來的文章,你會繼續(xù)深入學習這些概念。并且介紹大部分編程語言都有的概念,比如變量、數(shù)據(jù)類型和函數(shù),以及如何在 Rust 中使用它們。
到此這篇關于Rust寫個猜數(shù)字游戲的文章就介紹到這了,更多相關Rust猜數(shù)字游戲內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!