猜謎游戲

2022-04-21 10:04 更新

猜謎游戲

我們的第一個(gè)項(xiàng)目,將實(shí)現(xiàn)一個(gè)典型的初學(xué)者編程的問題:猜謎游戲。下面介紹下它是如何工作的:我們的程序?qū)⑸梢粋€(gè)從一到一百的隨機(jī)整數(shù)。然后它會(huì)提示我們輸入一個(gè)猜測(cè)值。依據(jù)我們的輸入的猜測(cè)值,它會(huì)告訴我們猜測(cè)值是否過低或者過高。一旦我們猜正確,它將祝賀我們。聽起來不錯(cuò)吧?

設(shè)置

進(jìn)入你的項(xiàng)目目錄,讓我們建立一個(gè)新項(xiàng)目。還記得我們必須為 hello_world 創(chuàng)建目錄結(jié)構(gòu)和 Cargo.toml 嗎?Cargo 有一個(gè)命令能為我們做這些事。讓我們來試一試:

    $ cd ~/projects
    $ cargo new guessing_game --bin
    $ cd guessing_game

我們向命令 cargo new 傳遞了我們項(xiàng)目的名稱,然后添加了標(biāo)記 --bin ,這是因?yàn)槲覀冋趧?chuàng)建一個(gè)二進(jìn)制文件,而不是一個(gè)庫文件。

查看生成的 Cargo.toml:

    [package]

    name = "guessing_game"
    version = "0.1.0"
    authors = ["Your Name <you@example.com>"]

Cargo 從你的環(huán)境中得到這些信息。如果它是不正確的,進(jìn)入文件并且改正過來。

最后,Cargo 為我們生成一個(gè)“Hello,world!”。查看下 src/main.rs

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

讓我們嘗試編譯下 Cargo 給我們的東西:

    $ cargo build
       Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)

太好了!再次打開你的 src/main.rs。我們將在這個(gè)文件中編寫所有的代碼。

在我們繼續(xù)之前,讓我教你一個(gè) Cargo 的命令: run.cargo run 是有點(diǎn)像 cargo build 的命令,但是它可以在生成的可執(zhí)行文件后運(yùn)行此文件。試一試:

    $ cargo run
       Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
         Running `target/debug/guessing_game`
    Hello, world!

太棒了!當(dāng)你需要在一個(gè)項(xiàng)目中快速迭代時(shí),這個(gè) run 命令可以派上用場(chǎng)。我們游戲就是這樣的一個(gè)項(xiàng)目,在下一次迭代之前,我們需要快速測(cè)試每個(gè)迭代。

處理一個(gè)猜測(cè)值

我們開始做吧!對(duì)于我們的猜謎游戲,我們需要做的第一件事就是允許玩家輸入一個(gè)猜測(cè)值。將下面的內(nèi)容輸入到你的 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)
            .ok()
            .expect("Failed to read line");

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

這里面有很多代碼??!讓我們一點(diǎn)一點(diǎn)地輸入它。

    use std::io;

我們需要獲取用戶的輸入,然后將打印結(jié)果作為輸出。因此,我們需要標(biāo)準(zhǔn)庫中的 io 庫。Rust 利用“序部”只要引入少量的東西到每一個(gè)項(xiàng)目,。如果在序部中沒有的話,那么你必須直接 use 它。

    fn main() {

正如之前你所見過的, main() 函數(shù)是程序的入口點(diǎn)。這個(gè) fn 語法聲明了一個(gè)新函數(shù), ()表明沒有參數(shù), {表示函數(shù)的主體開始。因?yàn)槲覀儾话粋€(gè)返回類型,它假定是 (),一個(gè)空元組。

       println!("Guess the number!");

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

我們之前學(xué)習(xí)到的 println!() 是一個(gè)打印字符串到屏幕上的宏。

    let mut guess = String::new();

現(xiàn)在越來越有趣了!有很多東西在這個(gè)小行上。首先要注意的是,這是一個(gè)let 語句,是用來創(chuàng)建變量綁定的。他們以這種形式存在:

    let foo = bar;

這將創(chuàng)建一個(gè)名為 foo 的新綁定,并將其綁定到值 bar 上。在許多語言中,這被稱為一個(gè)“變量”,但是 Rust 的變量綁定有其成熟的方法。

例如,他們?cè)谀J(rèn)情況下是不可變的。這就是我們的例子中使用 mut 的原因:它使一個(gè)綁定是可變,而不是不可改變的。 let 的左邊不能添加名稱,實(shí)際上,它可接受一個(gè)“模式”。之后,我們將更多的使用模式。現(xiàn)在它很容易使用:

    let foo = 5; // 不可變的.
    let mut bar = 5; // 可變的

哦, // 之后將開始一個(gè)注釋,直到行的結(jié)束。Rust 編譯時(shí),將會(huì)忽略所有的注釋。

所以現(xiàn)在我們知道了 let mut guess 將引入一個(gè)名為 gusss 可變的綁定,但我們必須看 = 另一邊的綁定: String::new()。

String 是一個(gè)字符串類型,由標(biāo)準(zhǔn)庫提供的。 String 是一個(gè)可增長(zhǎng),utf-8 編碼的文本。

::new() 語法使用 ::因?yàn)檫@是一個(gè)特定類型的關(guān)聯(lián)函數(shù)。也就是說,它與 String 本身相關(guān),而不是一個(gè)特定的實(shí)例化的 String。一些語言稱之為“靜態(tài)方法”。

這個(gè)函數(shù)命名為 new(),因?yàn)樗鼊?chuàng)建了一個(gè)新的,空的 String。你會(huì)在許多類型上發(fā)現(xiàn) new()函數(shù),因?yàn)樗莿?chuàng)建某類型新值的一個(gè)普通的名稱。

讓我們繼續(xù)往下看:

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

這又有很多代碼!讓我們一點(diǎn)一點(diǎn)的看。第一行包含兩部分。這是第一部分:

    io::stdin()

還記得我們?nèi)绾卧诔绦虻牡谝恍惺褂?use std::io 嗎?我們現(xiàn)在調(diào)用一個(gè)與之相關(guān)的函數(shù)。如果我們不用 use std::io,我們可以這樣寫這一行 std::io::stdin()。

這個(gè)特別的函數(shù)返回一個(gè)句柄到終端的標(biāo)準(zhǔn)輸入。更具體地說,返回一個(gè) std::io::Stdin。

下一部分將使用這個(gè)句柄獲取來自用戶的輸入:

    .read_line(&mut guess)

在這里,我們?cè)诰浔险{(diào)用 read_line() 方法。方法就像聯(lián)合的方法,但其只能在一個(gè)特定的實(shí)例類型中使用,而不是在類型本身。我們還傳遞了一個(gè)參數(shù)到 read_line(): &mut guess。

還記得我們上面是怎么綁定 guess 的嗎?我們認(rèn)為它是可變的。然而, read_line 不是將一個(gè) String 作為一個(gè)參數(shù):它用 &mut String 做參數(shù)。Rust 有一個(gè)稱為“引用”的特性,對(duì)一個(gè)數(shù)據(jù)你可以有多個(gè)引用,這樣可以減少復(fù)制。引用是一個(gè)復(fù)雜的特性。如何安全和容易的使用引用是 Rust 的一個(gè)主要賣點(diǎn)?,F(xiàn)在我們不需要知道很多這些細(xì)節(jié)來完成我們的項(xiàng)目?,F(xiàn)在我們所需要知道的是像 let 綁定,引用是默認(rèn)是不可變的之類的事情。因此,我們需要寫代碼 &mut guess,而不是 &guess。

為什么 read_line() 采用一個(gè)可變的字符串引用做參數(shù)? 它的工作就是獲取用戶鍵入到標(biāo)準(zhǔn)輸入的內(nèi)容,并將轉(zhuǎn)換成一個(gè)字符串。因此,以該字符串作為參數(shù),因?yàn)橐砑虞斎?,所以它需要是可變的?/p>

但是我們不確定這一行代碼會(huì)執(zhí)行正確。雖然它僅是一行文本,但是它是第一部分的單一邏輯行代碼:

        .ok()
        .expect("Failed to read line");

當(dāng)你用類似 .foo() 的語法調(diào)用一個(gè)方法時(shí),你可以用換行和空格來寫開啟新的一行代碼。這能幫助你分開很長(zhǎng)的代碼行。我們之前的代碼是這樣寫的:

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

但是這樣的代碼閱讀性不好,所以我們將其分隔開。三個(gè)方法調(diào)用分成三行來寫。我們已經(jīng)講過 read_line() 了,那么 ok() 和 expect() 是什么呢?嗯,我們之前已經(jīng)提到了, read_line() 讀取了用戶輸入到 &mut String 中的內(nèi)容。而且它也返回了一個(gè)值:在上面的例子中,返回值是 io::Result。Rust 在其標(biāo)準(zhǔn)庫中有很多類型命名為 Result:一個(gè)通用的 Result,然后用子庫指定具體的版本,比如 io::Result

使用這些 Result 類型的目的是編碼錯(cuò)誤處理信息。Result 類型的值,類似于任何類型,其有定義的方法。在上面的例子中,io::Result 就有一個(gè) ok() 方法。這個(gè)方法表明,“我們想假定這個(gè)值是一個(gè)正確值。如果不是,那么就拋出錯(cuò)誤的信息?!蹦敲礊槭裁匆獟伋鏊??對(duì)于一個(gè)基本的程序,我們只是想打印出一個(gè)通用的錯(cuò)誤信息。任何這樣的基本問題的發(fā)生意味著程序不能繼續(xù)運(yùn)行了。ok() 方法返回了一個(gè)值,在這個(gè)值上定義了另外一個(gè)方法: expect()。 expect() 方法調(diào)用時(shí),會(huì)獲取一個(gè)值,如果這個(gè)值不是正確的值,那么會(huì)導(dǎo)致一個(gè)應(yīng)急(panic!)的發(fā)生,你要傳遞一個(gè)信息給這個(gè)應(yīng)急。這樣的一個(gè)應(yīng)急(panic!)會(huì)導(dǎo)致我們的程序崩潰,并且會(huì)顯示你傳遞的信息。

如果我們不調(diào)用這兩個(gè)方法,程序能編譯成功,但我們會(huì)得到一個(gè)警告信息:

    $ cargo build
       Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
    src/main.rs:10:5: 10:39 warning: unused result which must be used,
    #[warn(unused_must_use)] on by default
    src/main.rs:10     io::stdin().read_line(&mut guess);

Rust 警告我們,我們沒有使用 Result 值。這個(gè)警告來自 io::Result 所有的一個(gè)特殊的注釋。Rust 試圖告訴你,你沒有處理一個(gè)有可能的錯(cuò)誤。避免這個(gè)錯(cuò)誤的正確方法是寫好錯(cuò)誤的處理部分的代碼。幸運(yùn)的是,如果程序有問題,而我們只是讓程序自己崩潰就好,那么我們可以用這兩個(gè)方法。但是如果我們想讓程序能從錯(cuò)誤中恢復(fù),那么我們就要做點(diǎn)其他的事情了。這部分內(nèi)容我們保留到未來的一個(gè)項(xiàng)目里再講。

第一個(gè)例子就剩下這一行代碼了:

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

這行代碼打印出我們輸入的字符串。{} 是一個(gè)占位符,所以我們把 guess 作為參數(shù)傳遞給它。如果我們有多個(gè) {} ,那么我們將傳遞多個(gè)參數(shù):

    let x = 5;
    let y = 10;

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

容易吧。

不管怎樣,這就是我們的學(xué)習(xí)過程啦。我們可以用我們所擁有的 cargo run 來運(yùn)行代碼了:

    $ cargo run
       Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
         Running `target/debug/guessing_game`
    Guess the number!
    Please input your guess.
    6
    You guessed: 6

好吧!我們的第一部分已經(jīng)完成了:我們可以從鍵盤輸入,然后將它打印出來。

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

接下來,我們需要生成一個(gè)秘密數(shù)字。Rust 還未引入有隨機(jī)數(shù)方法的標(biāo)準(zhǔn)庫。然而,Rust 的團(tuán)隊(duì)提供了一個(gè)隨機(jī)箱 rand crate。一個(gè)“箱”是一個(gè) Rust 的代碼包。我們已經(jīng)構(gòu)建了一個(gè)可執(zhí)行的“二進(jìn)制箱”。 rand 是一個(gè)“箱庫”,其中包含的代碼可以在其他程序中使用。

使用外部箱是 Cargo 的一個(gè)閃光點(diǎn)。在我們使用 rand 寫代碼之前,我們需要修改 Cargo.toml。打開它,并在文件底部添加這幾行:

    [dependencies]

    rand="0.3.0"

Cargo.toml 的 [dependencies] 的部分就像 [package] 部分一樣:直到下一個(gè)部分開始之前,它下面的內(nèi)容都是它所包含的。Cargo 通過使用依賴關(guān)系部分來知道你所擁有的依賴外部箱,以及你需要的版本。在本例中,我們使用的版本是 0.3.0。Cargo 能理解語義版本(Semantic Versioning),這是一個(gè)編寫版本號(hào)的標(biāo)準(zhǔn)。如果我們想使用最新的版本,我們可以使用 * ,或者我們也可以使用一個(gè)范圍的版本。Cargo 的文檔包含更多的細(xì)節(jié)內(nèi)容。

現(xiàn)在,在不改變我們的任何代碼,讓我們重新構(gòu)建我們的項(xiàng)目:

    $ cargo build
        Updating registry `https://github.com/rust-lang/crates.io-index`
     Downloading rand v0.3.8
     Downloading libc v0.1.6
       Compiling libc v0.1.6
       Compiling rand v0.3.8
       Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)

(你可以看到不同版本)。

有大量的新的內(nèi)容出現(xiàn)了!現(xiàn)在我們有一個(gè)外部依賴,Cargo 從注冊(cè)表獲取所有依賴的最新版本,我們這里的注冊(cè)表是一個(gè)來自 Crates.io 的數(shù)據(jù)副本。這里的 Crates.io 是 Rust 生態(tài)系統(tǒng)中人們發(fā)布給他人使用的開源 Rust 項(xiàng)目的地方。

更新注冊(cè)表,Cargo 會(huì)檢查我們的 [dependencies] 并下載我們沒有的一些依賴。在本例中,盡管我們說我們只想依賴 rand,但是我們也獲取了一份 libc 的拷貝。這是因?yàn)?rand 是基于 libc 工作的。下載完成后,Cargo 會(huì)先編譯這些依賴項(xiàng),然后再編譯我們的項(xiàng)目。

如果我們?cè)俅芜\(yùn)行 cargo build,我們將會(huì)得到不同的輸出:

    $ cargo build

沒有錯(cuò),這里是沒有輸出的!Cargo 知道我們的項(xiàng)目已經(jīng)構(gòu)建完成,并且項(xiàng)目所有的依賴項(xiàng)也構(gòu)建完成,因此沒有理由將所有這些事情再做一遍。既然無事可做,它就退出了。如果我們?cè)俅未蜷_ src/main.rs,做一個(gè)微不足道的改變,然后再保存它,再次運(yùn)行,我們就會(huì)看到一行:

    $ cargo build
       Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)

所以,我們告訴 Cargo 我們想要任何 0.3.x 版本的 rand,因此它會(huì)獲取當(dāng)時(shí)的最新版本,v0.3.8。但是當(dāng)下周有重要錯(cuò)誤修正的版本 v0.3.9 出現(xiàn)時(shí)會(huì)發(fā)生什么?雖然錯(cuò)誤修正是很重要的,但是如果 0.3.9 版本包含會(huì)破壞我們代碼的一個(gè)回歸呢?

這個(gè)問題的答案是鎖文件 Cargo.lock,現(xiàn)在你可以在你的項(xiàng)目目錄中找到它。當(dāng)你第一次構(gòu)建你的項(xiàng)目時(shí),Cargo 會(huì)找出所有的符合你的要求的版本,然后將他們寫到 Cargo.lock 文件中。當(dāng)未來你構(gòu)建項(xiàng)目時(shí),Cargo將會(huì)發(fā)現(xiàn) Cargo.lock 文件的存在,然后使用文件指定的版本,而不是再次做找出版本的工作。這可以讓你有一個(gè)可重復(fù)的自動(dòng)構(gòu)建。換句話說,直到我們明確升級(jí)之前,我們將待在 0.3.8 版本。所以那些分享我們的代碼人,也會(huì)感謝鎖文件的存在。

那么當(dāng)我們真的想使用 v0.3.9 版本時(shí),怎么辦?Cargo 還有另外一個(gè)命令, update,它的意思是“忽略鎖文件,找出適合我們指定要求的所有最新版本。如果可行,那么將那些版本信息寫到鎖文件中?!钡牵J(rèn)情況下,Cargo 只會(huì)比尋找大于 0.3.0 并且小于 0.4.0 的版本。如果我們想升級(jí)到 0.4.x 版本,那我們必須直接更新 Cargo.toml 文件。如果我們這么做了,我們下次運(yùn)行 cargo build 時(shí),Cargo 將更新索引并重新評(píng)估我們對(duì) rand 的需求。

這里提到了很多關(guān)于 Cargo 和它的生態(tài)系統(tǒng)的內(nèi)容,對(duì)現(xiàn)在來說,這就是我們所需要知道關(guān)于他們的全部信息。Cargo 使 Rust 很容易重復(fù)使用庫,所以 Rustaceans 傾向于編寫當(dāng)做子包組裝的小項(xiàng)目。

讓我們實(shí)際使用一下 rand。下面是我們要進(jìn)行的下一個(gè)步驟:

    extern crate rand;

    use std::io;
    use rand::Rng;

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

        let secret_number = rand::thread_rng().gen_range(1, 101);

        println!("The secret number is: {}", secret_number);

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

        let mut guess = String::new();

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

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

我們所做的第一件事就是改變了第一行代碼?,F(xiàn)在第一行是,extern crate rand。因?yàn)槲覀冊(cè)谖覀兊?[dependencies] 中聲明了 rand,所以我們可以使用 extern crate 讓 Rust 知道我們會(huì)使用它。這也相當(dāng)于一個(gè) use rand;,所以我們可以通過加前綴 rand::來利用 rand 箱中的所有東西。

接下來,我們添加了另一個(gè) use 行: use rand::Rng。一會(huì)兒我們要用一種方法,它需要 Rng 在作用域內(nèi)才運(yùn)行?;镜乃悸肥沁@樣的:方法是定義在一些所謂的“特征”上的,它需要特征在域內(nèi),才能使方法工作。更多的細(xì)節(jié)內(nèi)容,請(qǐng)閱讀特征章節(jié)。

在中間,有我們添加的另外兩行:

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_number);

我們使用 rand::thread_rng() 函數(shù)獲取隨機(jī)數(shù)產(chǎn)生器的一個(gè)副本,這是我們?cè)趫?zhí)行的一個(gè)本地線程。我們上面使用 use rand::Rng,它有一個(gè)可用的 gen_range() 方法。這個(gè)方法有兩個(gè)參數(shù),生成一個(gè)在兩個(gè)參數(shù)范圍內(nèi)的數(shù)字。它包括下界值,但不包括上界值,所以我們需要 1 和 101 兩個(gè)參數(shù),從而來獲取一個(gè)一到一百之間的數(shù)字。

第二行打印出了這個(gè)生成的秘密數(shù)字。這是在我們開發(fā)項(xiàng)目時(shí)是非常有用的,我們可以很容易地對(duì)其進(jìn)行測(cè)試。但是在最終版本里我們會(huì)刪除它。如果一開始的時(shí)候就打印出來你要猜的數(shù)字,顯然這不是一個(gè)游戲!

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

    $ cargo run
       Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
         Running `target/debug/guessing_game`
    Guess the number!
    The secret number is: 7
    Please input your guess.
    4
    You guessed: 4
    $ cargo run
         Running `target/debug/guessing_game`
    Guess the number!
    The secret number is: 83
    Please input your guess.
    5
    You guessed: 5

太棒了!下一篇:讓我們用我們的猜測(cè)值和秘密數(shù)字比較一下。

比較猜測(cè)值

現(xiàn)在,我們已經(jīng)得到了用戶的輸入,讓我們將我們猜測(cè)值與隨機(jī)值比較下。下面是我們要進(jìn)行的下一步,它現(xiàn)在還不能運(yùn)行:

    extern crate rand;

    use std::io;
    use std::cmp::Ordering;
    use rand::Rng;

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

        let secret_number = rand::thread_rng().gen_range(1, 101);

        println!("The secret number is: {}", secret_number);

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

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .ok()
            .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!"),
        }
    }

這里的有幾個(gè)新部分。第一個(gè)是另外的一個(gè) use。我們將叫做 std::cmp::Ordering 的類型放到了作用域內(nèi)。然后是底部新的五行代碼:

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

cmp() 方法可以在任何可比較的地方被調(diào)用,但是它需要你想要比較東西的一個(gè)引用。它將返回我們之前 use 的 Ordering 類型。我們使用一個(gè) match 語句來確定它到底是一個(gè)什么樣的 Ordering。 Ordering 是一個(gè)“枚舉”(enum,enumeration 的簡(jiǎn)稱)。枚舉看起來像這個(gè)樣子的:

    enum Foo {
        Bar,
        Baz,
    }

利用這個(gè)定義,任何類型是 Foo 的類型,可以是一個(gè) Foo::Bar 或者是一個(gè) Foo::Baz。我們使用 :: 表明一個(gè)特定 enum 變量的命名空間。

Ordering 枚舉有三個(gè)可能的變量: Less, Equal 和 Greater。 match 語句需要有一個(gè)類型的值輸入,并允許你為每一可能的值創(chuàng)建一個(gè)“手臂”。因?yàn)槲覀冇腥N類型的 Ordering,因此我們要三個(gè):

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

如果它是 Less,我們打印 Too small!,如果是 Greater,打印 Too big!,如果 Equal,打印 You win!。 match 是非常有用的,在 Rust 中經(jīng)常會(huì)使用它。

我之前提到了,項(xiàng)目現(xiàn)在還不能運(yùn)行。讓我們來試一試:

    $ cargo build
       Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
    src/main.rs:28:21: 28:35 error: mismatched types:
     expected `&collections::string::String`,
        found `&_`
    (expected struct `collections::string::String`,
        found integral variable) [E0308]
    src/main.rs:28     match guess.cmp(&secret_number) {

    error: aborting due to previous error
    Could not compile `guessing_game`.

這是一個(gè)很嚴(yán)重的錯(cuò)誤。它核心的意思是,我們有不匹配的類型。Rust 有一個(gè)強(qiáng)壯的、靜態(tài)的類型系統(tǒng)。然而,它也有類型推導(dǎo)過程。當(dāng)我們寫了 let guess = String::new(),Rust 能夠推斷出 guess 應(yīng)該是一個(gè) String,所以它不用讓我們寫出 guess 的類型。然而值從 1 到 100 的 secret_number 的類型就有很多了:i32,32 位的數(shù)字,u32,32 位無符號(hào)數(shù)字,或者 i64,一個(gè) 64 位的數(shù)字,或其的一些類型。Rust 的默認(rèn)類型是 i32。到目前為止,還沒有什么關(guān)系。然而在比較代碼中,Rust 不知道如何比較 guess 和 secret_number。他們需要是相同的類型。最終,我們把讀入的 String 轉(zhuǎn)換成一個(gè)數(shù)字類型,來進(jìn)行比較。我們用額外的三行來做這件事情。下面是我們的新程序:

    extern crate rand;

    use std::io;
    use std::cmp::Ordering;
    use rand::Rng;

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

        let secret_number = rand::thread_rng().gen_range(1, 101);

        println!("The secret number is: {}", secret_number);

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

        let mut guess = String::new();

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

        let guess: u32 = guess.trim().parse()
            .ok()
            .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()
        .ok()
        .expect("Please type a number!");

等一下,我想我們已經(jīng)有了一個(gè) guess 了吧?對(duì),我們確實(shí)有一個(gè),但是 Rust 允許我們用一個(gè)新的 guess 來“遮住”前面的那一個(gè)。在具體情況中這經(jīng)常使用。 guess 開始時(shí)是一個(gè) String,但我們想把它轉(zhuǎn)換成一個(gè) u32。遮蔽功能能讓我們重復(fù)使用 guess 這個(gè)名字,而不是迫使我們使用兩個(gè)唯一的名字如 guess_str ,guess,或者其他之類的。

類似于我們之前寫的代碼那樣,我們將 guess 綁定到一個(gè)表達(dá)式:

    guess.trim().parse()

緊隨其后的是一個(gè) ok().expect() 的調(diào)用。這里的 guess 指的是舊的那個(gè) guess,也就是我們輸入 String 到其中那一個(gè)。String 的 trim() 方法是消除 String 前端和后端所有空白字符串的方法。這是很重要的,因?yàn)槲覀儽仨毎础盎剀嚒辨I來結(jié)束 read_line() 方法的輸入。這意味著,如果我們鍵入 5 并且敲擊回車鍵,guess 看起來是這樣的:5\n。\n 表示“換行符”,回車鍵。trim() 方法就是用來去除類似這樣的東西的,讓我們的字符串只有 5。 字符串的 parse() 方法將字符串解析成某類數(shù)字。因?yàn)樗梢越馕龈鞣N數(shù)據(jù),所以我們需要給 Rust 提示我們想要轉(zhuǎn)化的確切數(shù)字類型。因此這樣寫,let guess: u32。guess 后面的冒號(hào)(:) 告訴 Rust 我們要轉(zhuǎn)化的類型。 u32 是一個(gè)無符號(hào) 32 位整數(shù)。Rust 有許多內(nèi)置數(shù)字類型,這里我們選擇了 u32。對(duì)于一個(gè)小的正整數(shù),這是一個(gè)很好的默認(rèn)選擇。

就像 read_line(),我們的 parse() 調(diào)用可能會(huì)導(dǎo)致一個(gè)錯(cuò)誤。如果我們的字符串包含了 A% 呢?這就沒有辦法轉(zhuǎn)換成一個(gè)數(shù)字了。因此,我們會(huì)像 read_line() 做的那樣:用 ok() 和 expect() 方法去處理崩潰,如果有一個(gè)錯(cuò)誤發(fā)生了的話。

讓我們來試試我們的新程序!

    $ cargo run
       Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
         Running `target/guessing_game`
    Guess the number!
    The secret number is: 58
    Please input your guess.
      76
    You guessed: 76
    Too big!

好了!你可以看到我甚至在猜的數(shù)字之前添加了空格,程序仍然發(fā)現(xiàn)我猜的數(shù)是 76。運(yùn)行幾次程序,再猜一個(gè)比較小的數(shù)字,驗(yàn)證猜測(cè)程序能正常運(yùn)行。

現(xiàn)在我們已經(jīng)做了游戲的大部分工作,但我們只能做一個(gè)猜測(cè)。讓我們通過添加循環(huán)來改變這一情況!

循環(huán)

loop 關(guān)鍵字為我們提供了一個(gè)無限循環(huán)。讓我們添加如下的代碼并嘗試一下:

    extern crate rand;

    use std::io;
    use std::cmp::Ordering;
    use rand::Rng;

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

        let secret_number = rand::thread_rng().gen_range(1, 101);

        println!("The secret number is: {}", secret_number);

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

            let mut guess = String::new();

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

            let guess: u32 = guess.trim().parse()
                .ok()
                .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!"),
            }
        }
    }

先等一下,我們只是添加一個(gè)無限循環(huán)么?是的,沒錯(cuò)。還記得我們關(guān)于 parse() 的講解嗎?如果我們給出了一個(gè)非數(shù)字的回答,它將會(huì) return 并且退出。我們來看:

    $ cargo run
       Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
         Running `target/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!'

像其他非數(shù)字的輸入一樣,quit 確實(shí)是退出了。嗯,至少可以說這是一個(gè)次優(yōu)的解決方案。第一個(gè)方案:當(dāng)你贏得比賽時(shí),程序退出:

    extern crate rand;

    use std::io;
    use std::cmp::Ordering;
    use rand::Rng;

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

        let secret_number = rand::thread_rng().gen_range(1, 101);

        println!("The secret number is: {}", secret_number);

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

            let mut guess = String::new();

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

            let guess: u32 = guess.trim().parse()
                .ok()
                .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;
                }
            }
        }
    }

通過在打印 You win!的代碼后面添加一行 break,當(dāng)我們贏了的時(shí)候就會(huì)退出循環(huán)。退出循環(huán)也意味著退出了程序,因?yàn)樗?main() 做的最后一件事。這里我們想做一個(gè)額外的調(diào)整:當(dāng)有人輸入非數(shù)字時(shí),我們并不退出,只是忽略它。我們可以這樣做:

    extern crate rand;

    use std::io;
    use std::cmp::Ordering;
    use rand::Rng;

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

        let secret_number = rand::thread_rng().gen_range(1, 101);

        println!("The secret number is: {}", secret_number);

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

            let mut guess = String::new();

            io::stdin().read_line(&mut guess)
                .ok()
                .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;
                }
            }
        }
    }

這些代碼改變了:

    let guess: u32 = match guess.trim().parse() {
        Ok(num) => num,
        Err(_) => continue,
    };

通過將使用 ok().expect() 換做使用一個(gè) match 語句,這可以教會(huì)你是如何從“崩潰錯(cuò)誤”轉(zhuǎn)變到“處理錯(cuò)誤”的。parse() 返回的 Result 是一個(gè)像 Ordering 一樣的枚舉,但在這里,每個(gè)變量都有與之相關(guān)的一些數(shù)據(jù): Ok 是一個(gè) success, Err 是一個(gè) failure。它們每個(gè)又包含更多的信息:成功的解析成整數(shù),一個(gè)錯(cuò)誤的類型。在這種情況下,我們將匹配 match 在 Ok(num),這里將 Ok 的內(nèi)部值的名字設(shè)置為 num,然后我們?cè)谟覀?cè)返回它的值。在 Err 的情況下,我們不關(guān)心它是什么樣的錯(cuò)誤,所以這里我們只用一個(gè)下劃線 ‘_’,而不是一個(gè)名字。這樣會(huì)忽略掉錯(cuò)誤,continue 會(huì)繼續(xù)進(jìn)行下一個(gè)循環(huán)(loop)的迭代。

現(xiàn)在,問題應(yīng)該都解決了!讓我們?cè)囈辉嚕?/p>

    $ cargo run
       Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
         Running `target/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!

太棒了!經(jīng)過最后一個(gè)小小的調(diào)整,我們已經(jīng)完成了猜謎游戲。你能想到什么?沒錯(cuò),我們不想打印出秘密數(shù)字。打印這個(gè)數(shù)字對(duì)于測(cè)試來說是很棒的一件事,但是當(dāng)運(yùn)行時(shí),它會(huì)毀掉這個(gè)游戲的。下面是我們最終的代碼:

    extern crate rand;

    use std::io;
    use std::cmp::Ordering;
    use rand::Rng;

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

        let secret_number = rand::thread_rng().gen_range(1, 101);

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

            let mut guess = String::new();

            io::stdin().read_line(&mut guess)
                .ok()
                .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;
                }
            }
        }
    }

完成了!

此時(shí)此刻,你已經(jīng)成功地構(gòu)建了猜謎游戲!恭喜你!

這第一個(gè)項(xiàng)目向你展示了很多內(nèi)容: let, match,方法,相關(guān)函數(shù),使用外部箱等等。我們的下一個(gè)項(xiàng)目將展示更多的內(nèi)容。

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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)