99re热这里只有精品视频,7777色鬼xxxx欧美色妇,国产成人精品一区二三区在线观看,内射爽无广熟女亚洲,精品人妻av一区二区三区

Rust 寫個(gè)猜數(shù)字游戲

2023-03-22 15:07 更新
ch02-00-guessing-game-tutorial.md
commit 6e2fe7c0f085989cc498cec139e717e2af172cb7

讓我們一起動(dòng)手完成一個(gè)項(xiàng)目,來(lái)快速上手 Rust!本章將介紹 Rust 中一些常用概念,并通過(guò)真實(shí)的程序來(lái)展示如何運(yùn)用它們。你將會(huì)學(xué)到 let、match、方法(method)、關(guān)聯(lián)函數(shù)(associated function)、使用外部 crate 等知識(shí)!后續(xù)章節(jié)會(huì)深入探討這些概念的細(xì)節(jié)。在這一章,我們將練習(xí)基礎(chǔ)內(nèi)容。

我們會(huì)實(shí)現(xiàn)一個(gè)經(jīng)典的新手編程問(wèn)題:猜猜看游戲。它是這么工作的:程序?qū)?huì)隨機(jī)生成一個(gè) 1 到 100 之間的隨機(jī)整數(shù)。接著它會(huì)請(qǐng)玩家猜一個(gè)數(shù)并輸入,然后提示猜測(cè)是大了還是小了。如果猜對(duì)了,它會(huì)打印祝賀信息并退出。

準(zhǔn)備一個(gè)新項(xiàng)目

要?jiǎng)?chuàng)建一個(gè)新項(xiàng)目,進(jìn)入第一章中創(chuàng)建的 projects 目錄,使用 Cargo 新建一個(gè)項(xiàng)目,如下:

$ cargo new guessing_game
$ cd guessing_game

第一個(gè)命令,cargo new,它獲取項(xiàng)目的名稱(guessing_game)作為第一個(gè)參數(shù)。第二個(gè)命令進(jìn)入到新創(chuàng)建的項(xià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 生成了一個(gè) “Hello, world!” 程序。查看 src/main.rs 文件:

文件名: src/main.rs

fn main() {
    println!("Hello, world!");
}

現(xiàn)在使用 cargo run 命令,一步完成 “Hello, world!” 程序的編譯和運(yùn)行:

$ 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!

當(dāng)你需要在項(xiàng)目中快速迭代時(shí),run 命令就能派上用場(chǎng),正如我們?cè)谶@個(gè)游戲項(xiàng)目中做的,在下一次迭代之前快速測(cè)試每一次迭代。

重新打開 src/main.rs 文件。我們將會(huì)在這個(gè)文件中編寫全部的代碼。

處理一次猜測(cè)

猜猜看程序的第一部分請(qǐng)求和處理用戶輸入,并檢查輸入是否符合預(yù)期的格式。首先,允許玩家輸入猜測(cè)。在 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:獲取用戶猜測(cè)并打印的代碼

這些代碼包含很多信息,我們一行一行地過(guò)一遍。為了獲取用戶輸入并打印結(jié)果作為輸出,我們需要將 io輸入/輸出庫(kù)引入當(dāng)前作用域。io 庫(kù)來(lái)自于標(biāo)準(zhǔn)庫(kù),也被稱為 std

use std::io;

默認(rèn)情況下,Rust 設(shè)定了若干個(gè)會(huì)自動(dòng)導(dǎo)入到每個(gè)程序作用域中的標(biāo)準(zhǔn)庫(kù)內(nèi)容,這組內(nèi)容被稱為 預(yù)導(dǎo)入(preclude) 內(nèi)容。你可以在標(biāo)準(zhǔn)庫(kù)文檔中查看預(yù)導(dǎo)入的所有內(nèi)容。

如果你需要的類型不在預(yù)導(dǎo)入內(nèi)容中,就必須使用 use 語(yǔ)句顯式地將其引入作用域。std::io 庫(kù)提供很多有用的功能,包括接收用戶輸入的功能。

如第一章所提及,main 函數(shù)是程序的入口點(diǎn):

fn main() {

fn 語(yǔ)法聲明了一個(gè)新函數(shù),小括號(hào) () 表明沒(méi)有參數(shù),大括號(hào) { 作為函數(shù)體的開始。

第一章也提及了 println! 是一個(gè)在屏幕上打印字符串的宏:

    println!("Guess the number!");

    println!("Please input your guess.");

這些代碼僅僅打印提示,介紹游戲的內(nèi)容然后請(qǐng)求用戶輸入。

使用變量?jī)?chǔ)存值

接下來(lái),創(chuàng)建一個(gè) 變量variable)來(lái)儲(chǔ)存用戶輸入,像這樣:

    let mut guess = String::new();

現(xiàn)在程序開始變得有意思了!這一小行代碼發(fā)生了很多事。我們使用 let 語(yǔ)句來(lái)創(chuàng)建變量。這里是另外一個(gè)例子:

let apples = 5;

這行代碼新建了一個(gè)叫做 apples 的變量并把它綁定到值 5 上。在 Rust 中,變量默認(rèn)是不可變的,這意味著一旦我們給變量賦值,這個(gè)值就不再可以修改了。我們將會(huì)在第三章的 “變量與可變性” 部分詳細(xì)討論這個(gè)概念。下面的例子展示了如何在變量名前使用 mut 來(lái)使一個(gè)變量可變:

let apples = 5; // 不可變
let mut bananas = 5; // 可變

注意:?//? 語(yǔ)法開始一個(gè)注釋,持續(xù)到行尾。Rust 忽略注釋中的所有內(nèi)容,第三章將會(huì)詳細(xì)介紹注釋。

回到猜猜看程序中?,F(xiàn)在我們知道了 let mut guess 會(huì)引入一個(gè)叫做 guess 的可變變量。等號(hào)(=)告訴Rust我們現(xiàn)在想將某個(gè)值綁定在變量上。等號(hào)的右邊是 guess 所綁定的值,它是 String::new 的結(jié)果,這個(gè)函數(shù)會(huì)返回一個(gè) String 的新實(shí)例。String 是一個(gè)標(biāo)準(zhǔn)庫(kù)提供的字符串類型,它是 UTF-8 編碼的可增長(zhǎng)文本塊。

::new 那一行的 :: 語(yǔ)法表明 new 是 String 類型的一個(gè) 關(guān)聯(lián)函數(shù)associated function)。關(guān)聯(lián)函數(shù)是針對(duì)類型實(shí)現(xiàn)的,在這個(gè)例子中是 String,而不是 String 的某個(gè)特定實(shí)例。一些語(yǔ)言中把它稱為 靜態(tài)方法static method)。

new 函數(shù)創(chuàng)建了一個(gè)新的空字符串,你會(huì)發(fā)現(xiàn)很多類型上有 new 函數(shù),因?yàn)樗莿?chuàng)建類型實(shí)例的慣用函數(shù)名。

總的來(lái)說(shuō),let mut guess = String::new(); 這一行創(chuàng)建了一個(gè)可變變量,當(dāng)前它綁定到一個(gè)新的 String 空實(shí)例上。

接收用戶輸入

回憶一下,我們?cè)诔绦虻牡谝恍惺褂?nbsp;use std::io; 從標(biāo)準(zhǔn)庫(kù)中引入了輸入/輸出功能?,F(xiàn)在調(diào)用 io 庫(kù)中的函數(shù) stdin

    io::stdin()
        .read_line(&mut guess)

如果程序的開頭沒(méi)有使用 use std::io 引入 io 庫(kù),我們?nèi)钥梢酝ㄟ^(guò)把函數(shù)調(diào)用寫成 std::io::stdin 來(lái)使用函數(shù)。stdin 函數(shù)返回一個(gè) std::io::Stdin 的實(shí)例,這代表終端標(biāo)準(zhǔn)輸入句柄的類型。

代碼的下一部分,.read_line(&mut guess),調(diào)用 read_line 方法從標(biāo)準(zhǔn)輸入句柄獲取用戶輸入。我們還將 &mut guess 作為參數(shù)傳遞給 read_line() 函數(shù),讓其將用戶輸入儲(chǔ)存到這個(gè)字符串中。read_line 的工作是,無(wú)論用戶在標(biāo)準(zhǔn)輸入中鍵入什么內(nèi)容,都將其追加(不會(huì)覆蓋其原有內(nèi)容)到一個(gè)字符串中,因此它需要字符串作為參數(shù)。這個(gè)字符串參數(shù)應(yīng)該是可變的,以便 read_line 將用戶輸入附加上去。

& 表示這個(gè)參數(shù)是一個(gè) 引用reference),它允許多處代碼訪問(wèn)同一處數(shù)據(jù),而無(wú)需在內(nèi)存中多次拷貝。引用是一個(gè)復(fù)雜的特性,Rust 的一個(gè)主要優(yōu)勢(shì)就是安全而簡(jiǎn)單的操縱引用。完成當(dāng)前程序并不需要了解如此多細(xì)節(jié)。現(xiàn)在,我們只需知道它像變量一樣,默認(rèn)是不可變的。因此,需要寫成 &mut guess 來(lái)使其可變,而不是 &guess。(第四章會(huì)更全面的解釋引用。)

使用 Result 類型來(lái)處理潛在的錯(cuò)誤

我們還沒(méi)有完全分析完這行代碼。雖然我們已經(jīng)講到了第三行代碼,但要注意:它仍是邏輯行(雖然換行了但仍是語(yǔ)句)的一部分。后一部分是這個(gè)方法(method):

        .expect("Failed to read line");

我們也可以將代碼這樣寫:

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

不過(guò),過(guò)長(zhǎng)的代碼行難以閱讀,所以最好拆開來(lái)寫。通常來(lái)說(shuō),當(dāng)使用 .method_name() 語(yǔ)法調(diào)用方法時(shí)引入換行符和空格將長(zhǎng)的代碼行拆開是明智的?,F(xiàn)在來(lái)看看這行代碼干了什么。

之前提到了 read_line 會(huì)將用戶輸入附加到傳遞給它的字符串中,不過(guò)它也會(huì)返回一個(gè)類型為 Result 的值。 Result 是一種枚舉類型,通常也寫作 enum。枚舉類型變量的值可以是多種可能狀態(tài)中的一個(gè)。我們把每種可能的狀態(tài)稱為一種 枚舉成員(variant)。

第六章將介紹枚舉的更多細(xì)節(jié)。這里的 Result 類型將用來(lái)編碼錯(cuò)誤處理的信息。

Result 的成員是 Ok 和 ErrOk 成員表示操作成功,內(nèi)部包含成功時(shí)產(chǎn)生的值。Err 成員則意味著操作失敗,并且包含失敗的前因后果。

這些 Result 類型的作用是編碼錯(cuò)誤處理信息。Result 類型的值,像其他類型一樣,擁有定義于其上的方法。Result 的實(shí)例擁有 expect 方法。如果 io::Result 實(shí)例的值是 Err,expect 會(huì)導(dǎo)致程序崩潰,并顯示當(dāng)做參數(shù)傳遞給 expect 的信息。如果 read_line 方法返回 Err,則可能是來(lái)源于底層操作系統(tǒng)錯(cuò)誤的結(jié)果。如果 Result 實(shí)例的值是 Ok,expect 會(huì)獲取 Ok 中的值并原樣返回。在本例中,這個(gè)值是用戶輸入到標(biāo)準(zhǔn)輸入中的字節(jié)數(shù)。

如果不調(diào)用 expect,程序也能編譯,不過(guò)會(huì)出現(xiàn)一個(gè)警告:

$ 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 警告我們沒(méi)有使用 read_line 的返回值 Result,說(shuō)明有一個(gè)可能的錯(cuò)誤沒(méi)有處理。

消除警告的正確做法是實(shí)際去編寫錯(cuò)誤處理代碼,不過(guò)由于我們就是希望程序在出現(xiàn)問(wèn)題時(shí)立即崩潰,所以直接使用 expect第九章 會(huì)學(xué)習(xí)如何從錯(cuò)誤中恢復(fù)。

使用 println! 占位符打印值

除了位于結(jié)尾的右花括號(hào),目前為止就只有這一行代碼值得討論一下了,就是這一行:

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

這行代碼現(xiàn)在打印了存儲(chǔ)用戶輸入的字符串。第一個(gè)參數(shù)是格式化字符串,里面的 {} 是預(yù)留在特定位置的占位符:把 {} 想象成小蟹鉗,可以?shī)A住合適的值。使用 {} 也可以打印多個(gè)值:第一對(duì) {} 使用格式化字符串之后的第一個(gè)值,第二對(duì)則使用第二個(gè)值,依此類推。調(diào)用一次 println! 打印多個(gè)值看起來(lái)像這樣:

let x = 5;
let y = 10;

println!("x = {} and y = {}", x, y);

這行代碼會(huì)打印出 x = 5 and y = 10。

測(cè)試第一部分代碼

讓我們來(lái)測(cè)試下猜猜看游戲的第一部分。使用 cargo run 運(yùn)行:

$ 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)完成:我們從鍵盤獲取輸入并打印了出來(lái)。

生成一個(gè)秘密數(shù)字

接下來(lái),需要生成一個(gè)秘密數(shù)字,好讓用戶來(lái)猜。秘密數(shù)字應(yīng)該每次都不同,這樣重復(fù)玩才不會(huì)乏味;范圍應(yīng)該在 1 到 100 之間,這樣才不會(huì)太困難。Rust 標(biāo)準(zhǔn)庫(kù)中尚未包含隨機(jī)數(shù)功能。然而,Rust 團(tuán)隊(duì)還是提供了一個(gè)包含上述功能的 rand crate。

使用 crate 來(lái)增加更多功能

記住,crate 是一個(gè) Rust 代碼包。我們正在構(gòu)建的項(xiàng)目是一個(gè) 二進(jìn)制 crate,它生成一個(gè)可執(zhí)行文件。 rand crate 是一個(gè) 庫(kù) crate,庫(kù) crate 可以包含任意能被其他程序使用的代碼,但是不能自執(zhí)行。

Cargo 對(duì)外部 crate 的運(yùn)用是其真正的亮點(diǎn)所在。在我們使用 rand 編寫代碼之前,需要修改 Cargo.toml 文件,引入一個(gè) rand 依賴?,F(xiàn)在打開這個(gè)文件并將下面這一行添加到 [dependencies] 片段標(biāo)題之下。在當(dāng)前版本下,請(qǐng)確保按照我們這里的方式指定 rand,否則本教程中的示例代碼可能無(wú)法工作。

文件名: Cargo.toml

rand = "0.8.3"

在 Cargo.toml 文件中,標(biāo)題以及之后的內(nèi)容屬同一個(gè)片段,直到遇到下一個(gè)標(biāo)題才開始新的片段。[dependencies] 片段告訴 Cargo 本項(xiàng)目依賴了哪些外部 crate 及其版本。本例中,我們使用語(yǔ)義化版本 0.8.3 來(lái)指定 rand crate。Cargo 理解 語(yǔ)義化版本(Semantic Versioning)(有時(shí)也稱為 SemVer),這是一種定義版本號(hào)的標(biāo)準(zhǔn)。0.8.3 事實(shí)上是 ^0.8.3 的簡(jiǎn)寫,它表示任何至少是 0.8.3 但小于 0.9.0 的版本。

Cargo 認(rèn)為這些版本與 0.8.3 版本的公有 API 相兼容,這樣的版本指定確保了我們可以獲取能使本章代碼編譯的最新的補(bǔ)?。╬atch)版本。任何大于等于 0.9.0 的版本不能保證和接下來(lái)的示例采用了相同的 API。

現(xiàn)在,不修改任何代碼,構(gòu)建項(xiàng)目,如示例 2-2 所示:

示例 2-2: 將 rand crate 添加為依賴之后運(yùn)行 cargo build 的輸出

可能會(huì)出現(xiàn)不同的版本號(hào)(多虧了語(yǔ)義化版本,它們與代碼是兼容的!),同時(shí)顯示順序也可能會(huì)有所不同。

現(xiàn)在我們有了一個(gè)外部依賴,Cargo 從 registry 上獲取所有包的最新版本信息,這是一份來(lái)自 Crates.io 的數(shù)據(jù)拷貝。Crates.io 是 Rust 生態(tài)環(huán)境中的開發(fā)者們向他人貢獻(xiàn) Rust 開源項(xiàng)目的地方。

在更新完 registry 后,Cargo 檢查 [dependencies] 片段并下載列表中包含但還未下載的 crates 。本例中,雖然只聲明了 rand 一個(gè)依賴,然而 Cargo 還是額外獲取了 rand 所需要的其他 crates,因?yàn)?nbsp;rand 依賴它們來(lái)正常工作。下載完成后,Rust 編譯依賴,然后使用這些依賴編譯項(xiàng)目。

如果不做任何修改,立刻再次運(yùn)行 cargo build,則不會(huì)看到任何除了 Finished 行之外的輸出。Cargo 知道它已經(jīng)下載并編譯了依賴,同時(shí) Cargo.toml 文件也沒(méi)有變動(dòng)。Cargo 還知道代碼也沒(méi)有任何修改,所以它不會(huì)重新編譯代碼。因?yàn)闊o(wú)事可做,它簡(jiǎn)單的退出了。

如果打開 src/main.rs 文件,做一些無(wú)關(guān)緊要的修改,保存并再次構(gòu)建,則會(huì)出現(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 只針對(duì) src/main.rs 文件的微小修改而更新構(gòu)建。依賴沒(méi)有變化,所以 Cargo 知道它可以復(fù)用已經(jīng)為此下載并編譯的代碼。它只是重新構(gòu)建了部分(項(xiàng)目)代碼。

Cargo.lock 文件確保構(gòu)建是可重現(xiàn)的

Cargo 有一個(gè)機(jī)制來(lái)確保任何人在任何時(shí)候重新構(gòu)建代碼,都會(huì)產(chǎn)生相同的結(jié)果:Cargo 只會(huì)使用你指定的依賴版本,除非你又手動(dòng)指定了別的。例如,如果下周 rand crate 的 0.8.4 版本出來(lái)了,它修復(fù)了一個(gè)重要的 bug,同時(shí)也含有一個(gè)會(huì)破壞代碼運(yùn)行的缺陷。為了處理這個(gè)問(wèn)題,Rust在你第一次運(yùn)行 cargo build 時(shí)建立了 Cargo.lock 文件,我們現(xiàn)在可以在guessing_game 目錄找到它。

當(dāng)?shù)谝淮螛?gòu)建項(xiàng)目時(shí),Cargo 計(jì)算出所有符合要求的依賴版本并寫入 Cargo.lock 文件。當(dāng)將來(lái)構(gòu)建項(xiàng)目時(shí),Cargo 會(huì)發(fā)現(xiàn) Cargo.lock 已存在并使用其中指定的版本,而不是再次計(jì)算所有的版本。這使得你擁有了一個(gè)自動(dòng)化的可重現(xiàn)的構(gòu)建。換句話說(shuō),項(xiàng)目會(huì)持續(xù)使用 0.8.3 直到你顯式升級(jí),多虧有了 Cargo.lock 文件。由于 Cargo.lock 文件對(duì)于“可重復(fù)構(gòu)建”非常重要,因此它通常會(huì)和項(xiàng)目中的其余代碼一樣納入到版本控制系統(tǒng)中。

更新 crate 到一個(gè)新版本

當(dāng)你 確實(shí) 需要升級(jí) crate 時(shí),Cargo 提供了這樣一個(gè)命令,update,它會(huì)忽略 Cargo.lock 文件,并計(jì)算出所有符合 Cargo.toml 聲明的最新版本。Cargo 接下來(lái)會(huì)把這些版本寫入 Cargo.lock 文件。不過(guò),Cargo 默認(rèn)只會(huì)尋找大于 0.8.3 而小于 0.9.0 的版本。如果 rand crate 發(fā)布了兩個(gè)新版本,0.8.4 和 0.9.0,在運(yùn)行 cargo update 時(shí)會(huì)出現(xiàn)如下內(nèi)容:

$ cargo update
    Updating crates.io index
    Updating rand v0.8.3 -> v0.8.4

Cargo 忽略了 0.9.0 版本。這時(shí),你也會(huì)注意到的 Cargo.lock 文件中的變化無(wú)外乎現(xiàn)在使用的 rand crate 版本是0.8.4 。如果想要使用 0.9.0 版本的 rand 或是任何 0.9.x 系列的版本,必須像這樣更新 Cargo.toml 文件:

[dependencies]

rand = "0.9.0"

下一次運(yùn)行 cargo build 時(shí),Cargo 會(huì)從 registry 更新可用的 crate,并根據(jù)你指定的新版本重新計(jì)算。

第十四章會(huì)講到 Cargo 及其生態(tài)系統(tǒng) 的更多內(nèi)容,不過(guò)目前你只需要了解這么多。通過(guò) Cargo 復(fù)用庫(kù)文件非常容易,因此 Rustacean 能夠編寫出由很多包組裝而成的更輕巧的項(xiàng)目。

生成一個(gè)隨機(jī)數(shù)

讓我們開始使用 rand 來(lái)生成一個(gè)猜猜看隨機(jī)數(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:添加生成隨機(jī)數(shù)的代碼

首先,我們新增了一行 use rand::RngRng 是一個(gè) trait,它定義了隨機(jī)數(shù)生成器應(yīng)實(shí)現(xiàn)的方法,想使用這些方法的話,此 trait 必須在作用域中。第十章會(huì)詳細(xì)介紹 trait。

接下來(lái),我們?cè)谥虚g還新增加了兩行。第一行調(diào)用了 rand::thread_rng 函數(shù)提供實(shí)際使用的隨機(jī)數(shù)生成器:它位于當(dāng)前執(zhí)行線程的本地環(huán)境中,并從操作系統(tǒng)獲取 seed。接著調(diào)用隨機(jī)數(shù)生成器的 gen_range 方法。這個(gè)方法由 use rand::Rng 語(yǔ)句引入到作用域的 Rng trait 定義。gen_range 方法獲取一個(gè)范圍表達(dá)式(range expression)作為參數(shù),并生成一個(gè)在此范圍之間的隨機(jī)數(shù)。這里使用的這類范圍表達(dá)式使用了 start..=end 這樣的形式,也就是說(shuō)包含了上下端點(diǎn),所以需要指定 1..=100 來(lái)請(qǐng)求一個(gè) 1 和 100 之間的數(shù)。

注意:你不可能憑空就知道應(yīng)該 use 哪個(gè) trait 以及該從 crate 中調(diào)用哪個(gè)方法,因此每個(gè)crate 有使用說(shuō)明文檔。Cargo 有一個(gè)很棒的功能是:運(yùn)行 ?cargo doc --open? 命令來(lái)構(gòu)建所有本地依賴提供的文檔,并在瀏覽器中打開。例如,假設(shè)你對(duì) ?rand ?crate 中的其他功能感興趣,你可以運(yùn)行 ?cargo doc --open? 并點(diǎn)擊左側(cè)導(dǎo)航欄中的 ?rand?。

新增加的第二行代碼打印出了秘密數(shù)字。這在開發(fā)程序時(shí)很有用,因?yàn)榭梢詼y(cè)試它,不過(guò)在最終版本中會(huì)刪掉它。如果游戲一開始就打印出結(jié)果就沒(méi)什么可玩的了!

嘗試運(yùn)行程序幾次:

$ 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)該能得到不同的隨機(jī)數(shù),同時(shí)它們應(yīng)該都是在 1 和 100 之間的。干得漂亮!

比較猜測(cè)的數(shù)字和秘密數(shù)字

現(xiàn)在有了用戶輸入和一個(gè)隨機(jī)數(shù),我們可以比較它們。這個(gè)步驟如示例 2-4 所示。注意這段代碼還不能通過(guò)編譯,我們稍后會(huì)解釋。

文件名: 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:處理比較兩個(gè)數(shù)字可能的返回值

首先我們?cè)黾恿肆硪粋€(gè) use 聲明,從標(biāo)準(zhǔn)庫(kù)引入了一個(gè)叫做 std::cmp::Ordering 的類型到作用域中。 Ordering 也是一個(gè)枚舉,不過(guò)它的成員是 Less、Greater 和 Equal。這是比較兩個(gè)值時(shí)可能出現(xiàn)的三種結(jié)果。

接著,底部的五行新代碼使用了 Ordering 類型,cmp 方法用來(lái)比較兩個(gè)值并可以在任何可比較的值上調(diào)用。它獲取一個(gè)被比較值的引用:這里是把 guess 與 secret_number 做比較。 然后它會(huì)返回一個(gè)剛才通過(guò) use 引入作用域的 Ordering 枚舉的成員。使用一個(gè) match 表達(dá)式,根據(jù)對(duì) guess 和 secret_number 調(diào)用 cmp 返回的 Ordering 成員來(lái)決定接下來(lái)做什么。

一個(gè) match 表達(dá)式由 分支(arms) 構(gòu)成。一個(gè)分支包含一個(gè) 模式pattern)和表達(dá)式開頭的值與分支模式相匹配時(shí)應(yīng)該執(zhí)行的代碼。Rust 獲取提供給 match 的值并挨個(gè)檢查每個(gè)分支的模式。match 結(jié)構(gòu)和模式是 Rust 中強(qiáng)大的功能,它體現(xiàn)了代碼可能遇到的多種情形,并幫助你確保沒(méi)有遺漏處理。這些功能將分別在第六章和第十八章詳細(xì)介紹。

讓我們看看使用 match 表達(dá)式的例子。假設(shè)用戶猜了 50,這時(shí)隨機(jī)生成的秘密數(shù)字是 38。比較 50 與 38 時(shí),因?yàn)?50 比 38 要大,cmp 方法會(huì)返回 Ordering::GreaterOrdering::Greater 是 match 表達(dá)式得到的值。它檢查第一個(gè)分支的模式,Ordering::Less 與 Ordering::Greater并不匹配,所以它忽略了這個(gè)分支的代碼并來(lái)到下一個(gè)分支。下一個(gè)分支的模式是 Ordering::Greater,正確 匹配!這個(gè)分支關(guān)聯(lián)的代碼被執(zhí)行,在屏幕打印出 Too big!match 表達(dá)式會(huì)在第一次成功匹配后終止,因?yàn)樵搱?chǎng)景下沒(méi)有檢查最后一個(gè)分支的必要。

然而,示例 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

錯(cuò)誤的核心表明這里有 不匹配的類型mismatched types)。Rust 有一個(gè)靜態(tài)強(qiáng)類型系統(tǒng),同時(shí)也有類型推斷。當(dāng)我們寫出 let guess = String::new() 時(shí),Rust 推斷出 guess 應(yīng)該是 String 類型,并不需要我們寫出類型。另一方面,secret_number,是數(shù)字類型。幾個(gè)數(shù)字類型擁有 1 到 100 之間的值:32 位數(shù)字 i32;32 位無(wú)符號(hào)數(shù)字 u32;64 位數(shù)字 i64 等等。Rust 默認(rèn)使用 i32,所以它是 secret_number 的類型,除非增加類型信息,或任何能讓 Rust 推斷出不同數(shù)值類型的信息。這里錯(cuò)誤的原因在于 Rust 不會(huì)比較字符串類型和數(shù)字類型。

所以我們必須把從輸入中讀取到的 String 轉(zhuǎn)換為一個(gè)真正的數(shù)字類型,才好與秘密數(shù)字進(jìn)行比較。這可以通過(guò)在 main 函數(shù)體中增加如下代碼來(lái)實(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)建了一個(gè)叫做 guess 的變量。不過(guò)等等,不是已經(jīng)有了一個(gè)叫做 guess 的變量了嗎?確實(shí)如此,不過(guò) Rust 允許用一個(gè)新值來(lái) 隱藏 (shadow) guess 之前的值。這個(gè)功能常用在需要轉(zhuǎn)換值類型之類的場(chǎng)景。它允許我們復(fù)用 guess 變量的名字,而不是被迫創(chuàng)建兩個(gè)不同變量,諸如 guess_str 和 guess 之類。(第三章會(huì)介紹 shadowing 的更多細(xì)節(jié)。)

我們將這個(gè)新變量綁定到 guess.trim().parse() 表達(dá)式上。表達(dá)式中的 guess 指的是包含輸入的字符串類型 guess 變量。String 實(shí)例的 trim 方法會(huì)去除字符串開頭和結(jié)尾的空白字符,我們必須執(zhí)行此方法才能將字符串與 u32 比較,因?yàn)?nbsp;u32 只能包含數(shù)值型數(shù)據(jù)。用戶必須輸入 enter 鍵才能讓 read_line 返回并輸入他們的猜想,這將會(huì)在字符串中增加一個(gè)換行(newline)符。例如,用戶輸入 5 并按下 enter(在 Windows 上,按下 enter 鍵會(huì)得到一個(gè)回車符和一個(gè)換行符,\r\n),guess 看起來(lái)像這樣:5\n 或者 5\r\n。\n 代表 “換行”,回車鍵;\r 代表 “回車”,回車鍵。trim 方法會(huì)消除 \n 或者 \r\n,只留下 5。

字符串的 parse 方法 將字符串轉(zhuǎn)換成其他類型。這里用它來(lái)把字符串轉(zhuǎn)換為數(shù)值。我們需要告訴 Rust 具體的數(shù)字類型,這里通過(guò) let guess: u32 指定。guess 后面的冒號(hào)(:)告訴 Rust 我們指定了變量的類型。Rust 有一些內(nèi)建的數(shù)字類型;u32 是一個(gè)無(wú)符號(hào)的 32 位整型。對(duì)于不大的正整數(shù)來(lái)說(shuō),它是不錯(cuò)的默認(rèn)類型,第三章還會(huì)講到其他數(shù)字類型。另外,程序中的 u32 注解以及與 secret_number 的比較,意味著 Rust 會(huì)推斷出 secret_number 也是 u32 類型?,F(xiàn)在可以使用相同類型比較兩個(gè)值了!

parse 方法只有在字符邏輯上可以轉(zhuǎn)換為數(shù)字的時(shí)候才能工作所以非常容易出錯(cuò)。例如,字符串中包含 A%,就無(wú)法將其轉(zhuǎn)換為一個(gè)數(shù)字。因此,parse 方法返回一個(gè) Result 類型。像之前 “使用 Result 類型來(lái)處理潛在的錯(cuò)誤” 討論的 read_line 方法那樣,再次按部就班的用 expect 方法處理即可。如果 parse 不能從字符串生成一個(gè)數(shù)字,返回一個(gè) Result 的 Err 成員時(shí),expect 會(huì)使游戲崩潰并打印附帶的信息。如果 parse 成功地將字符串轉(zhuǎn)換為一個(gè)數(shù)字,它會(huì)返回 Result 的 Ok 成員,然后 expect 會(huì)返回 Ok 值中的數(shù)字。

現(xiàn)在讓我們運(yù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!

漂亮!即便是在猜測(cè)之前添加了空格,程序依然能判斷出用戶猜測(cè)了 76。多運(yùn)行程序幾次,輸入不同的數(shù)字來(lái)檢驗(yàn)不同的行為:猜一個(gè)正確的數(shù)字,猜一個(gè)過(guò)大的數(shù)字和猜一個(gè)過(guò)小的數(shù)字。

現(xiàn)在游戲已經(jīng)大體上能玩了,不過(guò)用戶只能猜一次。增加一個(gè)循環(huán)來(lái)改變它吧!

使用循環(huán)來(lái)允許多次猜測(cè)

loop 關(guān)鍵字創(chuàng)建了一個(gè)無(wú)限循環(huán)。我們會(huì)增加循環(huán)來(lái)給用戶更多機(jī)會(huì)猜數(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!"),
        }
    }
}

如上所示,我們將提示用戶猜測(cè)之后的所有內(nèi)容移動(dòng)到了循環(huán)中。確保 loop 循環(huán)中的代碼多縮進(jìn)四個(gè)空格,再次運(yùn)行程序。注意這里有一個(gè)新問(wèn)題,因?yàn)槌绦蛑覍?shí)地執(zhí)行了我們的要求:永遠(yuǎn)地請(qǐng)求另一個(gè)猜測(cè),用戶好像無(wú)法退出啊!

用戶總能使用 ctrl-c 終止程序。不過(guò)還有另一個(gè)方法跳出無(wú)限循環(huán),就是 “比較猜測(cè)與秘密數(shù)字” 部分提到的 parse:如果用戶輸入的答案不是一個(gè)數(shù)字,程序會(huì)崩潰。我們可以利用這一點(diǎn)來(lái)退出,如下所示:

$ 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 將會(huì)退出程序,同時(shí)你會(huì)注意到其他任何非數(shù)字輸入也一樣。然而,這并不理想,我們想要當(dāng)猜測(cè)正確的數(shù)字時(shí)游戲停止。

猜測(cè)正確后退出

讓我們?cè)黾右粋€(gè) break 語(yǔ)句,在用戶猜對(duì)時(shí)退出游戲:

文件名: 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;
            }
        }
    }
}

通過(guò)在 You win! 之后增加一行 break,用戶猜對(duì)了神秘?cái)?shù)字后會(huì)退出循環(huán)。退出循環(huán)也意味著退出程序,因?yàn)檠h(huán)是 main 的最后一部分。

處理無(wú)效輸入

為了進(jìn)一步改善游戲性,不要在用戶輸入非數(shù)字時(shí)崩潰,需要忽略非數(shù)字,讓用戶可以繼續(xù)猜測(cè)。可以通過(guò)修改 guess 將 String 轉(zhuǎn)化為 u32 那部分代碼來(lái)實(shí)現(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ù)字的猜測(cè)并重新請(qǐng)求數(shù)字而不是讓程序崩潰

我們將 expect 調(diào)用換成 match 語(yǔ)句,以從遇到錯(cuò)誤就崩潰轉(zhuǎn)換為處理錯(cuò)誤。須知 parse 返回一個(gè) Result 類型,而 Result 是一個(gè)擁有 Ok 或 Err 成員的枚舉。這里使用的 match 表達(dá)式,和之前處理 cmp 方法返回 Ordering 時(shí)用的一樣。

如果 parse 能夠成功的將字符串轉(zhuǎn)換為一個(gè)數(shù)字,它會(huì)返回一個(gè)包含結(jié)果數(shù)字的 Ok。這個(gè) Ok 值與 match 第一個(gè)分支的模式相匹配,該分支對(duì)應(yīng)的動(dòng)作返回 Ok 值中的數(shù)字 num,最后如愿變成新創(chuàng)建的 guess 變量。

如果 parse 能將字符串轉(zhuǎn)換為一個(gè)數(shù)字,它會(huì)返回一個(gè)包含更多錯(cuò)誤信息的 Err。Err 值不能匹配第一個(gè) match 分支的 Ok(num) 模式,但是會(huì)匹配第二個(gè)分支的 Err(_) 模式:_ 是一個(gè)通配符值,本例中用來(lái)匹配所有 Err 值,不管其中有何種信息。所以程序會(huì)執(zhí)行第二個(gè)分支的動(dòng)作,continue 意味著進(jìn)入 loop 的下一次循環(huán),請(qǐng)求另一個(gè)猜測(cè)。這樣程序就有效的忽略了 parse 可能遇到的所有錯(cuò)誤!

現(xiàn)在萬(wàn)事俱備,只需運(yù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!

太棒了!再有最后一個(gè)小的修改,就能完成猜猜看游戲了:還記得程序依然會(huì)打印出秘密數(shù)字。在測(cè)試時(shí)還好,但正式發(fā)布時(shí)會(huì)毀了游戲。刪掉打印秘密數(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:猜猜看游戲的完整代碼

總結(jié)

此時(shí)此刻,你順利完成了猜猜看游戲。恭喜!

本項(xiàng)目通過(guò)動(dòng)手實(shí)踐,向你介紹了 Rust 新概念:let、match、函數(shù)、使用外部 crate 等等,接下來(lái)的幾章,你會(huì)繼續(xù)深入學(xué)習(xí)這些概念。第三章介紹大部分編程語(yǔ)言都有的概念,比如變量、數(shù)據(jù)類型和函數(shù),以及如何在 Rust 中使用它們。第四章探索所有權(quán)(ownership),這是一個(gè) Rust 同其他語(yǔ)言大不相同的功能。第五章討論結(jié)構(gòu)體和方法的語(yǔ)法,而第六章側(cè)重解釋枚舉。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)