ch02-00-guessing-game-tutorial.md
commit 6e2fe7c0f085989cc498cec139e717e2af172cb7
讓我們一起動手完成一個項目,來快速上手 Rust!本章將介紹 Rust 中一些常用概念,并通過真實的程序來展示如何運用它們。你將會學到 let
、match
、方法(method)、關聯(lián)函數(shù)(associated function)、使用外部 crate 等知識!后續(xù)章節(jié)會深入探討這些概念的細節(jié)。在這一章,我們將練習基礎內容。
我們會實現(xiàn)一個經(jīng)典的新手編程問題:猜猜看游戲。它是這么工作的:程序將會隨機生成一個 1 到 100 之間的隨機整數(shù)。接著它會請玩家猜一個數(shù)并輸入,然后提示猜測是大了還是小了。如果猜對了,它會打印祝賀信息并退出。
要創(chuàng)建一個新項目,進入第一章中創(chuàng)建的 projects 目錄,使用 Cargo 新建一個項目,如下:
$ cargo new guessing_game
$ cd guessing_game
第一個命令,cargo new
,它獲取項目的名稱(guessing_game
)作為第一個參數(shù)。第二個命令進入到新創(chuàng)建的項目目錄。
看看生成的 Cargo.toml 文件:
文件名: 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]
正如第一章那樣,cargo new
生成了一個 “Hello, world!” 程序。查看 src/main.rs 文件:
文件名: src/main.rs
fn main() {
println!("Hello, world!");
}
現(xiàn)在使用 cargo run
命令,一步完成 “Hello, world!” 程序的編譯和運行:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Hello, world!
當你需要在項目中快速迭代時,run
命令就能派上用場,正如我們在這個游戲項目中做的,在下一次迭代之前快速測試每一次迭代。
重新打開 src/main.rs 文件。我們將會在這個文件中編寫全部的代碼。
猜猜看程序的第一部分請求和處理用戶輸入,并檢查輸入是否符合預期的格式。首先,允許玩家輸入猜測。在 src/main.rs 中輸入示例 2-1 中的代碼。
文件名: 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}");
}
示例 2-1:獲取用戶猜測并打印的代碼
這些代碼包含很多信息,我們一行一行地過一遍。為了獲取用戶輸入并打印結果作為輸出,我們需要將 io
輸入/輸出庫引入當前作用域。io
庫來自于標準庫,也被稱為 std
:
use std::io;
默認情況下,Rust 設定了若干個會自動導入到每個程序作用域中的標準庫內容,這組內容被稱為 預導入(preclude) 內容。你可以在標準庫文檔中查看預導入的所有內容。
如果你需要的類型不在預導入內容中,就必須使用 use
語句顯式地將其引入作用域。std::io
庫提供很多有用的功能,包括接收用戶輸入的功能。
如第一章所提及,main
函數(shù)是程序的入口點:
fn main() {
fn
語法聲明了一個新函數(shù),小括號 ()
表明沒有參數(shù),大括號 {
作為函數(shù)體的開始。
第一章也提及了 println!
是一個在屏幕上打印字符串的宏:
println!("Guess the number!");
println!("Please input your guess.");
這些代碼僅僅打印提示,介紹游戲的內容然后請求用戶輸入。
接下來,創(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;
從標準庫中引入了輸入/輸出功能。現(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)勢就是安全而簡單的操縱引用。完成當前程序并不需要了解如此多細節(jié)?,F(xiàn)在,我們只需知道它像變量一樣,默認是不可變的。因此,需要寫成 &mut guess
來使其可變,而不是 &guess
。(第四章會更全面的解釋引用。)
我們還沒有完全分析完這行代碼。雖然我們已經(jīng)講到了第三行代碼,但要注意:它仍是邏輯行(雖然換行了但仍是語句)的一部分。后一部分是這個方法(method):
.expect("Failed to read line");
我們也可以將代碼這樣寫:
io::stdin().read_line(&mut guess).expect("Failed to read line");
不過,過長的代碼行難以閱讀,所以最好拆開來寫。通常來說,當使用 .method_name()
語法調用方法時引入換行符和空格將長的代碼行拆開是明智的?,F(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)一個警告:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Rust 警告我們沒有使用 read_line
的返回值 Result
,說明有一個可能的錯誤沒有處理。
消除警告的正確做法是實際去編寫錯誤處理代碼,不過由于我們就是希望程序在出現(xiàn)問題時立即崩潰,所以直接使用 expect
。第九章 會學習如何從錯誤中恢復。
除了位于結尾的右花括號,目前為止就只有這一行代碼值得討論一下了,就是這一行:
println!("You guessed: {guess}");
這行代碼現(xiàn)在打印了存儲用戶輸入的字符串。第一個參數(shù)是格式化字符串,里面的 {}
是預留在特定位置的占位符:把 {}
想象成小蟹鉗,可以夾住合適的值。使用 {}
也可以打印多個值:第一對 {}
使用格式化字符串之后的第一個值,第二對則使用第二個值,依此類推。調用一次 println!
打印多個值看起來像這樣:
let x = 5;
let y = 10;
println!("x = {} and y = {}", x, y);
這行代碼會打印出 x = 5 and y = 10
。
讓我們來測試下猜猜看游戲的第一部分。使用 cargo run
運行:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
至此為止,游戲的第一部分已經(jīng)完成:我們從鍵盤獲取輸入并打印了出來。
接下來,需要生成一個秘密數(shù)字,好讓用戶來猜。秘密數(shù)字應該每次都不同,這樣重復玩才不會乏味;范圍應該在 1 到 100 之間,這樣才不會太困難。Rust 標準庫中尚未包含隨機數(shù)功能。然而,Rust 團隊還是提供了一個包含上述功能的 rand
crate。
記住,crate 是一個 Rust 代碼包。我們正在構建的項目是一個 二進制 crate,它生成一個可執(zhí)行文件。 rand
crate 是一個 庫 crate,庫 crate 可以包含任意能被其他程序使用的代碼,但是不能自執(zhí)行。
Cargo 對外部 crate 的運用是其真正的亮點所在。在我們使用 rand
編寫代碼之前,需要修改 Cargo.toml 文件,引入一個 rand
依賴?,F(xiàn)在打開這個文件并將下面這一行添加到 [dependencies]
片段標題之下。在當前版本下,請確保按照我們這里的方式指定 rand
,否則本教程中的示例代碼可能無法工作。
文件名: Cargo.toml
rand = "0.8.3"
在 Cargo.toml 文件中,標題以及之后的內容屬同一個片段,直到遇到下一個標題才開始新的片段。[dependencies]
片段告訴 Cargo 本項目依賴了哪些外部 crate 及其版本。本例中,我們使用語義化版本 0.8.3
來指定 rand
crate。Cargo 理解 語義化版本(Semantic Versioning)(有時也稱為 SemVer),這是一種定義版本號的標準。0.8.3
事實上是 ^0.8.3
的簡寫,它表示任何至少是 0.8.3
但小于 0.9.0
的版本。
Cargo 認為這些版本與 0.8.3
版本的公有 API 相兼容,這樣的版本指定確保了我們可以獲取能使本章代碼編譯的最新的補?。╬atch)版本。任何大于等于 0.9.0
的版本不能保證和接下來的示例采用了相同的 API。
現(xiàn)在,不修改任何代碼,構建項目,如示例 2-2 所示:
示例 2-2: 將 rand crate 添加為依賴之后運行 cargo build
的輸出
可能會出現(xiàn)不同的版本號(多虧了語義化版本,它們與代碼是兼容的!),同時顯示順序也可能會有所不同。
現(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 build
,則不會看到任何除了 Finished
行之外的輸出。Cargo 知道它已經(jīng)下載并編譯了依賴,同時 Cargo.toml 文件也沒有變動。Cargo 還知道代碼也沒有任何修改,所以它不會重新編譯代碼。因為無事可做,它簡單的退出了。
如果打開 src/main.rs 文件,做一些無關緊要的修改,保存并再次構建,則會出現(xiàn)兩行輸出:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
這一行表示 Cargo 只針對 src/main.rs 文件的微小修改而更新構建。依賴沒有變化,所以 Cargo 知道它可以復用已經(jīng)為此下載并編譯的代碼。它只是重新構建了部分(項目)代碼。
Cargo 有一個機制來確保任何人在任何時候重新構建代碼,都會產(chǎn)生相同的結果:Cargo 只會使用你指定的依賴版本,除非你又手動指定了別的。例如,如果下周 rand
crate 的 0.8.4
版本出來了,它修復了一個重要的 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.3
直到你顯式升級,多虧有了 Cargo.lock 文件。由于 Cargo.lock 文件對于“可重復構建”非常重要,因此它通常會和項目中的其余代碼一樣納入到版本控制系統(tǒng)中。
當你 確實 需要升級 crate 時,Cargo 提供了這樣一個命令,update
,它會忽略 Cargo.lock 文件,并計算出所有符合 Cargo.toml 聲明的最新版本。Cargo 接下來會把這些版本寫入 Cargo.lock 文件。不過,Cargo 默認只會尋找大于 0.8.3
而小于 0.9.0
的版本。如果 rand
crate 發(fā)布了兩個新版本,0.8.4
和 0.9.0
,在運行 cargo update
時會出現(xiàn)如下內容:
$ cargo update
Updating crates.io index
Updating rand v0.8.3 -> v0.8.4
Cargo 忽略了 0.9.0
版本。這時,你也會注意到的 Cargo.lock 文件中的變化無外乎現(xiàn)在使用的 rand
crate 版本是0.8.4
。如果想要使用 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,如示例 2-3 所示。
文件名: 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}");
}
示例 2-3:添加生成隨機數(shù)的代碼
首先,我們新增了一行 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
?。
新增加的第二行代碼打印出了秘密數(shù)字。這在開發(fā)程序時很有用,因為可以測試它,不過在最終版本中會刪掉它。如果游戲一開始就打印出結果就沒什么可玩的了!
嘗試運行程序幾次:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
你應該能得到不同的隨機數(shù),同時它們應該都是在 1 和 100 之間的。干得漂亮!
現(xiàn)在有了用戶輸入和一個隨機數(shù),我們可以比較它們。這個步驟如示例 2-4 所示。注意這段代碼還不能通過編譯,我們稍后會解釋。
文件名: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
示例 2-4:處理比較兩個數(shù)字可能的返回值
首先我們增加了另一個 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
表達式會在第一次成功匹配后終止,因為該場景下沒有檢查最后一個分支的必要。
然而,示例 2-4 的代碼并不能編譯,可以嘗試一下:
$ cargo build
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.3
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| ^^^^^^^^^^^^^^ expected struct `String`, found integer
|
= note: expected reference `&String`
found reference `&{integer}`
error[E0283]: type annotations needed for `{integer}`
--> src/main.rs:8:44
|
8 | let secret_number = rand::thread_rng().gen_range(1..=100);
| ------------- ^^^^^^^^^ cannot infer type for type `{integer}`
| |
| consider giving `secret_number` a type
|
= note: multiple `impl`s satisfying `{integer}: SampleUniform` found in the `rand` crate:
- impl SampleUniform for i128;
- impl SampleUniform for i16;
- impl SampleUniform for i32;
- impl SampleUniform for i64;
and 8 more
note: required by a bound in `gen_range`
--> /Users/carolnichols/.cargo/registry/src/github.com-1ecc6299db9ec823/rand-0.8.3/src/rng.rs:129:12
|
129 | T: SampleUniform,
| ^^^^^^^^^^^^^ required by this bound in `gen_range`
help: consider specifying the type arguments in the function call
|
8 | let secret_number = rand::thread_rng().gen_range::<T, R>(1..=100);
| ++++++++
Some errors have detailed explanations: E0283, E0308.
For more information about an error, try `rustc --explain E0283`.
error: could not compile `guessing_game` due to 2 previous errors
錯誤的核心表明這里有 不匹配的類型(mismatched 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):
文件名: src/main.rs
// --snip--
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!"),
}
這行新代碼是:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
這里創(chuàng)建了一個叫做 guess
的變量。不過等等,不是已經(jīng)有了一個叫做 guess
的變量了嗎?確實如此,不過 Rust 允許用一個新值來 隱藏 (shadow) guess
之前的值。這個功能常用在需要轉換值類型之類的場景。它允許我們復用 guess
變量的名字,而不是被迫創(chuàng)建兩個不同變量,諸如 guess_str
和 guess
之類。(第三章會介紹 shadowing 的更多細節(jié)。)
我們將這個新變量綁定到 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
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
漂亮!即便是在猜測之前添加了空格,程序依然能判斷出用戶猜測了 76。多運行程序幾次,輸入不同的數(shù)字來檢驗不同的行為:猜一個正確的數(shù)字,猜一個過大的數(shù)字和猜一個過小的數(shù)字。
現(xiàn)在游戲已經(jīng)大體上能玩了,不過用戶只能猜一次。增加一個循環(huán)來改變它吧!
loop
關鍵字創(chuàng)建了一個無限循環(huán)。我們會增加循環(huán)來給用戶更多機會猜數(shù)字:
文件名: src/main.rs
// --snip--
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
// --snip--
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ù)字,程序會崩潰。我們可以利用這一點來退出,如下所示:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
輸入 quit
將會退出程序,同時你會注意到其他任何非數(shù)字輸入也一樣。然而,這并不理想,我們想要當猜測正確的數(shù)字時游戲停止。
讓我們增加一個 break
語句,在用戶猜對時退出游戲:
文件名: src/main.rs
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
通過在 You win!
之后增加一行 break
,用戶猜對了神秘數(shù)字后會退出循環(huán)。退出循環(huán)也意味著退出程序,因為循環(huán)是 main
的最后一部分。
為了進一步改善游戲性,不要在用戶輸入非數(shù)字時崩潰,需要忽略非數(shù)字,讓用戶可以繼續(xù)猜測??梢酝ㄟ^修改 guess
將 String
轉化為 u32
那部分代碼來實現(xiàn),如示例 2-5 所示:
文件名: src/main.rs
// --snip--
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}");
// --snip--
示例 2-5: 忽略非數(shù)字的猜測并重新請求數(shù)字而不是讓程序崩潰
我們將 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
:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 4.45s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
太棒了!再有最后一個小的修改,就能完成猜猜看游戲了:還記得程序依然會打印出秘密數(shù)字。在測試時還好,但正式發(fā)布時會毀了游戲。刪掉打印秘密數(shù)字的 println!
。示例 2-6 為最終代碼:
文件名: 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);
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;
}
}
}
}
示例 2-6:猜猜看游戲的完整代碼
此時此刻,你順利完成了猜猜看游戲。恭喜!
本項目通過動手實踐,向你介紹了 Rust 新概念:let
、match
、函數(shù)、使用外部 crate 等等,接下來的幾章,你會繼續(xù)深入學習這些概念。第三章介紹大部分編程語言都有的概念,比如變量、數(shù)據(jù)類型和函數(shù),以及如何在 Rust 中使用它們。第四章探索所有權(ownership),這是一個 Rust 同其他語言大不相同的功能。第五章討論結構體和方法的語法,而第六章側重解釋枚舉。
更多建議: