ch02-00-guessing-game-tutorial.md
commit 6e2fe7c0f085989cc498cec139e717e2af172cb7
讓我們一起動手完成一個項目,來快速上手 Rust!本章將介紹 Rust 中一些常用概念,并通過真實的程序來展示如何運用它們。你將會學到 let
、match
、方法(method)、關(guān)聯(lián)函數(shù)(associated function)、使用外部 crate 等知識!后續(xù)章節(jié)會深入探討這些概念的細節(jié)。在這一章,我們將練習基礎(chǔ)內(nèi)容。
我們會實現(xiàn)一個經(jīng)典的新手編程問題:猜猜看游戲。它是這么工作的:程序?qū)S機生成一個 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 文件。我們將會在這個文件中編寫全部的代碼。
猜猜看程序的第一部分請求和處理用戶輸入,并檢查輸入是否符合預(yù)期的格式。首先,允許玩家輸入猜測。在 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:獲取用戶猜測并打印的代碼
這些代碼包含很多信息,我們一行一行地過一遍。為了獲取用戶輸入并打印結(jié)果作為輸出,我們需要將 io
輸入/輸出庫引入當前作用域。io
庫來自于標準庫,也被稱為 std
:
use std::io;
默認情況下,Rust 設(shè)定了若干個會自動導入到每個程序作用域中的標準庫內(nèi)容,這組內(nèi)容被稱為 預(yù)導入(preclude) 內(nèi)容。你可以在標準庫文檔中查看預(yù)導入的所有內(nèi)容。
如果你需要的類型不在預(yù)導入內(nèi)容中,就必須使用 use
語句顯式地將其引入作用域。std::io
庫提供很多有用的功能,包括接收用戶輸入的功能。
如第一章所提及,main
函數(shù)是程序的入口點:
fn main() {
fn
語法聲明了一個新函數(shù),小括號 ()
表明沒有參數(shù),大括號 {
作為函數(shù)體的開始。
第一章也提及了 println!
是一個在屏幕上打印字符串的宏:
println!("Guess the number!");
println!("Please input your guess.");
這些代碼僅僅打印提示,介紹游戲的內(nèi)容然后請求用戶輸入。
接下來,創(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 忽略注釋中的所有內(nèi)容,第三章將會詳細介紹注釋。
回到猜猜看程序中?,F(xiàn)在我們知道了 let mut guess
會引入一個叫做 guess
的可變變量。等號(=
)告訴Rust我們現(xiàn)在想將某個值綁定在變量上。等號的右邊是 guess
所綁定的值,它是 String::new
的結(jié)果,這個函數(shù)會返回一個 String
的新實例。String
是一個標準庫提供的字符串類型,它是 UTF-8 編碼的可增長文本塊。
::new
那一行的 ::
語法表明 new
是 String
類型的一個 關(guān)聯(lián)函數(shù)(associated function)。關(guān)聯(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)在調(diào)用 io
庫中的函數(shù) stdin
:
io::stdin()
.read_line(&mut guess)
如果程序的開頭沒有使用 use std::io
引入 io
庫,我們?nèi)钥梢酝ㄟ^把函數(shù)調(diào)用寫成 std::io::stdin
來使用函數(shù)。stdin
函數(shù)返回一個 std::io::Stdin
的實例,這代表終端標準輸入句柄的類型。
代碼的下一部分,.read_line(&mut guess)
,調(diào)用 read_line
方法從標準輸入句柄獲取用戶輸入。我們還將 &mut guess
作為參數(shù)傳遞給 read_line()
函數(shù),讓其將用戶輸入儲存到這個字符串中。read_line
的工作是,無論用戶在標準輸入中鍵入什么內(nèi)容,都將其追加(不會覆蓋其原有內(nèi)容)到一個字符串中,因此它需要字符串作為參數(shù)。這個字符串參數(shù)應(yīng)該是可變的,以便 read_line
將用戶輸入附加上去。
&
表示這個參數(shù)是一個 引用(reference),它允許多處代碼訪問同一處數(shù)據(jù),而無需在內(nèi)存中多次拷貝。引用是一個復雜的特性,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()
語法調(diào)用方法時引入換行符和空格將長的代碼行拆開是明智的。現(xiàn)在來看看這行代碼干了什么。
之前提到了 read_line
會將用戶輸入附加到傳遞給它的字符串中,不過它也會返回一個類型為 Result
的值。 Result
是一種枚舉類型,通常也寫作 enum。枚舉類型變量的值可以是多種可能狀態(tài)中的一個。我們把每種可能的狀態(tài)稱為一種 枚舉成員(variant)。
第六章將介紹枚舉的更多細節(jié)。這里的 Result
類型將用來編碼錯誤處理的信息。
Result
的成員是 Ok
和 Err
,Ok
成員表示操作成功,內(nèi)部包含成功時產(chǎn)生的值。Err
成員則意味著操作失敗,并且包含失敗的前因后果。
這些 Result
類型的作用是編碼錯誤處理信息。Result
類型的值,像其他類型一樣,擁有定義于其上的方法。Result
的實例擁有 expect
方法。如果 io::Result
實例的值是 Err
,expect
會導致程序崩潰,并顯示當做參數(shù)傳遞給 expect
的信息。如果 read_line
方法返回 Err
,則可能是來源于底層操作系統(tǒng)錯誤的結(jié)果。如果 Result
實例的值是 Ok
,expect
會獲取 Ok
中的值并原樣返回。在本例中,這個值是用戶輸入到標準輸入中的字節(jié)數(shù)。
如果不調(diào)用 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
。第九章 會學習如何從錯誤中恢復。
除了位于結(jié)尾的右花括號,目前為止就只有這一行代碼值得討論一下了,就是這一行:
println!("You guessed: {guess}");
這行代碼現(xiàn)在打印了存儲用戶輸入的字符串。第一個參數(shù)是格式化字符串,里面的 {}
是預(yù)留在特定位置的占位符:把 {}
想象成小蟹鉗,可以夾住合適的值。使用 {}
也可以打印多個值:第一對 {}
使用格式化字符串之后的第一個值,第二對則使用第二個值,依此類推。調(diào)用一次 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ù)字應(yīng)該每次都不同,這樣重復玩才不會乏味;范圍應(yīng)該在 1 到 100 之間,這樣才不會太困難。Rust 標準庫中尚未包含隨機數(shù)功能。然而,Rust 團隊還是提供了一個包含上述功能的 rand
crate。
記住,crate 是一個 Rust 代碼包。我們正在構(gòu)建的項目是一個 二進制 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 文件中,標題以及之后的內(nèi)容屬同一個片段,直到遇到下一個標題才開始新的片段。[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)在,不修改任何代碼,構(gòu)建項目,如示例 2-2 所示:
示例 2-2: 將 rand crate 添加為依賴之后運行 cargo build
的輸出
可能會出現(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 build
,則不會看到任何除了 Finished
行之外的輸出。Cargo 知道它已經(jīng)下載并編譯了依賴,同時 Cargo.toml 文件也沒有變動。Cargo 還知道代碼也沒有任何修改,所以它不會重新編譯代碼。因為無事可做,它簡單的退出了。
如果打開 src/main.rs 文件,做一些無關(guān)緊要的修改,保存并再次構(gòu)建,則會出現(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 文件的微小修改而更新構(gòu)建。依賴沒有變化,所以 Cargo 知道它可以復用已經(jīng)為此下載并編譯的代碼。它只是重新構(gòu)建了部分(項目)代碼。
Cargo 有一個機制來確保任何人在任何時候重新構(gòu)建代碼,都會產(chǎn)生相同的結(jié)果:Cargo 只會使用你指定的依賴版本,除非你又手動指定了別的。例如,如果下周 rand
crate 的 0.8.4
版本出來了,它修復了一個重要的 bug,同時也含有一個會破壞代碼運行的缺陷。為了處理這個問題,Rust在你第一次運行 cargo build
時建立了 Cargo.lock 文件,我們現(xiàn)在可以在guessing_game 目錄找到它。
當?shù)谝淮螛?gòu)建項目時,Cargo 計算出所有符合要求的依賴版本并寫入 Cargo.lock 文件。當將來構(gòu)建項目時,Cargo 會發(fā)現(xiàn) Cargo.lock 已存在并使用其中指定的版本,而不是再次計算所有的版本。這使得你擁有了一個自動化的可重現(xiàn)的構(gòu)建。換句話說,項目會持續(xù)使用 0.8.3
直到你顯式升級,多虧有了 Cargo.lock 文件。由于 Cargo.lock 文件對于“可重復構(gòu)建”非常重要,因此它通常會和項目中的其余代碼一樣納入到版本控制系統(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)如下內(nèi)容:
$ 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) 的更多內(nèi)容,不過目前你只需要了解這么多。通過 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ù)生成器應(yīng)實現(xiàn)的方法,想使用這些方法的話,此 trait 必須在作用域中。第十章會詳細介紹 trait。
接下來,我們在中間還新增加了兩行。第一行調(diào)用了 rand::thread_rng
函數(shù)提供實際使用的隨機數(shù)生成器:它位于當前執(zhí)行線程的本地環(huán)境中,并從操作系統(tǒng)獲取 seed。接著調(diào)用隨機數(shù)生成器的 gen_range
方法。這個方法由 use rand::Rng
語句引入到作用域的 Rng
trait 定義。gen_range
方法獲取一個范圍表達式(range expression)作為參數(shù),并生成一個在此范圍之間的隨機數(shù)。這里使用的這類范圍表達式使用了 start..=end
這樣的形式,也就是說包含了上下端點,所以需要指定 1..=100
來請求一個 1 和 100 之間的數(shù)。
注意:你不可能憑空就知道應(yīng)該 use 哪個 trait 以及該從 crate 中調(diào)用哪個方法,因此每個crate 有使用說明文檔。Cargo 有一個很棒的功能是:運行 ?
cargo doc --open
? 命令來構(gòu)建所有本地依賴提供的文檔,并在瀏覽器中打開。例如,假設(shè)你對 ?rand
?crate 中的其他功能感興趣,你可以運行 ?cargo doc --open
? 并點擊左側(cè)導航欄中的 ?rand
?。
新增加的第二行代碼打印出了秘密數(shù)字。這在開發(fā)程序時很有用,因為可以測試它,不過在最終版本中會刪掉它。如果游戲一開始就打印出結(jié)果就沒什么可玩的了!
嘗試運行程序幾次:
$ 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
你應(yīng)該能得到不同的隨機數(shù),同時它們應(yīng)該都是在 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)的三種結(jié)果。
接著,底部的五行新代碼使用了 Ordering
類型,cmp
方法用來比較兩個值并可以在任何可比較的值上調(diào)用。它獲取一個被比較值的引用:這里是把 guess
與 secret_number
做比較。 然后它會返回一個剛才通過 use
引入作用域的 Ordering
枚舉的成員。使用一個 match 表達式,根據(jù)對 guess
和 secret_number
調(diào)用 cmp
返回的 Ordering
成員來決定接下來做什么。
一個 match
表達式由 分支(arms) 構(gòu)成。一個分支包含一個 模式(pattern)和表達式開頭的值與分支模式相匹配時應(yīng)該執(zhí)行的代碼。Rust 獲取提供給 match
的值并挨個檢查每個分支的模式。match
結(jié)構(gòu)和模式是 Rust 中強大的功能,它體現(xiàn)了代碼可能遇到的多種情形,并幫助你確保沒有遺漏處理。這些功能將分別在第六章和第十八章詳細介紹。
讓我們看看使用 match
表達式的例子。假設(shè)用戶猜了 50,這時隨機生成的秘密數(shù)字是 38。比較 50 與 38 時,因為 50 比 38 要大,cmp
方法會返回 Ordering::Greater
。Ordering::Greater
是 match
表達式得到的值。它檢查第一個分支的模式,Ordering::Less
與 Ordering::Greater
并不匹配,所以它忽略了這個分支的代碼并來到下一個分支。下一個分支的模式是 Ordering::Greater
,正確 匹配!這個分支關(guān)聯(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
應(yīng)該是 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
轉(zhuǎn)換為一個真正的數(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
之前的值。這個功能常用在需要轉(zhuǎn)換值類型之類的場景。它允許我們復用 guess
變量的名字,而不是被迫創(chuàng)建兩個不同變量,諸如 guess_str
和 guess
之類。(第三章會介紹 shadowing 的更多細節(jié)。)
我們將這個新變量綁定到 guess.trim().parse()
表達式上。表達式中的 guess
指的是包含輸入的字符串類型 guess
變量。String
實例的 trim
方法會去除字符串開頭和結(jié)尾的空白字符,我們必須執(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
方法 將字符串轉(zhuǎn)換成其他類型。這里用它來把字符串轉(zhuǎn)換為數(shù)值。我們需要告訴 Rust 具體的數(shù)字類型,這里通過 let guess: u32
指定。guess
后面的冒號(:
)告訴 Rust 我們指定了變量的類型。Rust 有一些內(nèi)建的數(shù)字類型;u32
是一個無符號的 32 位整型。對于不大的正整數(shù)來說,它是不錯的默認類型,第三章還會講到其他數(shù)字類型。另外,程序中的 u32
注解以及與 secret_number
的比較,意味著 Rust 會推斷出 secret_number
也是 u32
類型?,F(xiàn)在可以使用相同類型比較兩個值了!
parse
方法只有在字符邏輯上可以轉(zhuǎn)換為數(shù)字的時候才能工作所以非常容易出錯。例如,字符串中包含 A%
,就無法將其轉(zhuǎn)換為一個數(shù)字。因此,parse
方法返回一個 Result
類型。像之前 “使用 Result 類型來處理潛在的錯誤” 討論的 read_line
方法那樣,再次按部就班的用 expect
方法處理即可。如果 parse
不能從字符串生成一個數(shù)字,返回一個 Result
的 Err
成員時,expect
會使游戲崩潰并打印附帶的信息。如果 parse
成功地將字符串轉(zhuǎn)換為一個數(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
關(guān)鍵字創(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!"),
}
}
}
如上所示,我們將提示用戶猜測之后的所有內(nèi)容移動到了循環(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
轉(zhuǎn)化為 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
調(diào)用換成 match
語句,以從遇到錯誤就崩潰轉(zhuǎn)換為處理錯誤。須知 parse
返回一個 Result
類型,而 Result
是一個擁有 Ok
或 Err
成員的枚舉。這里使用的 match
表達式,和之前處理 cmp
方法返回 Ordering
時用的一樣。
如果 parse
能夠成功的將字符串轉(zhuǎn)換為一個數(shù)字,它會返回一個包含結(jié)果數(shù)字的 Ok
。這個 Ok
值與 match
第一個分支的模式相匹配,該分支對應(yīng)的動作返回 Ok
值中的數(shù)字 num
,最后如愿變成新創(chuàng)建的 guess
變量。
如果 parse
不能將字符串轉(zhuǎn)換為一個數(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 中使用它們。第四章探索所有權(quán)(ownership),這是一個 Rust 同其他語言大不相同的功能。第五章討論結(jié)構(gòu)體和方法的語法,而第六章側(cè)重解釋枚舉。
更多建議: