Rust學習筆記#1:一個猜謎遊戲小項目

在深刻探索Rust語法的細枝末節以前,先經過一個麻雀雖小但五臟俱全的小項目來總體把握Rust,這樣能夠避免迷失在細節的海洋中。咱們可能會經過這個小項目一會兒接觸到不少新概念,但沒必要驚慌,咱們只需淺嘗輒止對這些概念有個印象便可。git

《TRPL》(Rustaceans對《The Rust Programming Language》的愛稱)中就給出了一個經典的猜謎遊戲的例子,咱們一塊兒來學習它。編程

猜謎遊戲:程序將會隨機生成一個 1 到 100 之間的隨機整數。接着它會請玩家猜一個數並輸入,而後提示猜想是大了仍是小了。若是猜對了,它會打印祝賀信息並退出。安全

Rust如何管理項目

並非全部的代碼都像hello_world.rs同樣,一個文件就能夠搞定。一個項目每每具備複雜的代碼,咱們須要一種機制來管理這種複雜性,將一個項目切分紅若干小部分,每一個部分再進行切分,層層抽象,直到達到人腦能夠處理的規模。每種編程語言都有這樣的機制,例如Java的package機制,Rust也不例外。編程語言

Rust用了兩個概念來管理項目:一個是crate(項目),一個是mod(模塊)。模塊是用於在crate內部進行分層和封裝的機制,模塊內部能夠包含模塊。函數

  • crate:能夠簡單理解爲一個項目,crate是Rust中的獨立編譯單元(compile unit),每一個crate對應生成一個庫或者可執行文件。做爲對比,咱們比較熟悉的C語言中,一個單獨的.c文件和其全部的include文件組成一個編譯單元,每一個.c生成一個.o,而後將這些.o連接起來生成可執行文件。
  • mod:能夠簡單理解爲命名空間。mod能夠嵌套(注意crate之間不能出現循環引用),還能夠控制內部元素的可見性。

說到可見性問題,在Rust中,元素默認都是私有的,用pub關鍵字修飾的纔是公開的。公開和私有的訪問權限規定以下:oop

  • 若是一個元素是私有的,那麼只有本模塊內的元素以及它的子模塊能夠訪問
  • 若是一個元素是公開的,那麼能夠在本模塊外的做用域訪問它。

模塊是一種抽象的概念,文件是承載這個概念的實體,可是模塊和文件並非簡單的一一對應關係。在一個crate內部建立mod的方式有下面三種:學習

  • 在一個rs文件中建立內嵌模塊,直接使用mod關鍵字便可。
  • 獨立的一個rs文件就是一個模塊,文件名便是模塊名。
  • 一個文件夾也能夠建立一個模塊,文件夾內部要有一個xxx.rs文件,這個文件是這個模塊的入口。必需要在這個xxx.rs中聲明其子模塊,不然子模塊沒法被當成這個項目的源碼進行編譯。

新建項目

cargo不只是Rust的包管理器,還能夠用於建立項目。使用下列命令能夠建立一個名爲guessing_game的項目:優化

cargo new guessing_game --bin

注意,後面的--bin意味着咱們但願項目生成的是可執行程序,若是但願是library,則可使用--lib選項。ui

以上爲在命令行中手工建立項目,在Clion中能夠點擊File->New Project後以下圖填寫,而後點擊Create按鈕:編碼

項目結構

咱們可使用tree .命令或者直接在Clion中查看當前的文件夾結構,以下圖所示:

  • src/main.rs:這是cargo自動生成的rs文件。還記得前面講的crate和mod的概念嗎?在這個項目中,guessing_game是crate,src文件夾是mod,main.rssrcmod的入口。咱們能夠在srcmod中建立子模塊,但注意,這些子模塊都要在main.rs中聲明,不然沒法參與編譯。
  • .gitignore:這是git忽略文件,不懂其做用的同窗能夠自行搜索。這裏重點要說的是經過cargo new建立的項目自然就是一個git項目,這也印證了Rust對開源的擁護。
  • Cargo.toml:這是項目管理配置文件。TOML是一種很是簡潔好用的配置文件格式,TOML是Tom's Obvious, Minimal Language的首字母縮寫,這裏的Tom是Github的聯合創始人之一,感興趣的同窗能夠進一步自行了解TOML配置文件的寫法。
  • Cargo.lock:該文件包含項目依賴項的確切信息,由Cargo維護,咱們無須關心它。

編譯執行

src/main.rs裏cargo已經自動生成了輸出Hello, world!的代碼,咱們來運行一下它看可否正常輸出。

插一句題外話,在你往後漫長的Rust編碼生涯中,你會發現,你將在處理編譯錯誤上耗費大量的時間。還記得嗎,Rust 的一大特點是保證內存安全,這保證了Rust代碼只要運行起來就幾乎不會發生內存錯誤,這麼誘人的效果的背後的代價就是,咱們要在編碼時付出額外的努力。Rust爲了保證內存安全設計了一套複雜的規則,這致使咱們的代碼一不留神就會編譯不過。因此,在你往後常常用的一個操做就是檢查可否編譯經過,而不是直接編譯,由於直接編譯還要作代碼優化等會相對費時。可使用下列命令檢查編譯錯誤:

cargo check

在Clion中須要新加一個Configuration來執行cargo check命令,以下圖所示:

確保cargo check經過後,能夠執行cargo build來執行編譯。編譯後會產生一個target文件夾,在target/debug下會有一個和crate同名的可執行文件。但通常爲了方便,能夠直接執行cargo run,這條命令等價於先編譯後執行。下圖是執行cargo run後Clion的控制檯輸出:

猜謎遊戲

在完成了項目搭建後,接下來就要開始猜謎遊戲的代碼編寫了,我將它們分紅六部分:建立變量、輸入、輸出、錯誤處理、隨機數生成、完整代碼。

建立變量

使用let語句建立變量,須要注意的是,在Rust中,變量默認是不可變的,能夠在變量名前使用mut來使得變量可變:

let a = 5; // 不可變
let mut b = 10; // 可變

在上面的let語句中,咱們並無顯示聲明變量的類型,但這並不表明Rust是動態類型的,Rust仍然是靜態類型的,只不過Rust有一個能夠經過上下文推斷類型的強大編譯器。

輸出

咱們早已在hello_world.rs中見識過了最基本的輸出方式:

println!("Hello, world!");

須要注意的是,這裏的println!是一個宏,而非一個函數,println後面的感嘆號就是宏的標誌。Rust中的宏與C/C++中的宏是徹底不同的東西,簡單說,能夠把它理解爲一種安全版的編譯期語法擴展。這裏之因此使用輸出宏而非函數,是由於標準輸出宏能夠完成編譯期格式檢查,更加安全。

若是須要輸出某個變量的值,可使用佔位符{},例如:

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

其輸出結果爲:

x = 0 and y = 10

輸入

爲了從控制檯中獲取用戶的輸入,須要使用標準庫std::io。使用use語句將該庫引入當前做用域:

use std::io;

咱們可使用io庫中的函數stdin

let mut guess = String::new(); // 建立一個字符串類型的可變變量
io::stdin().read_line(&mut guess).expect("Failed to read line");

stdin函數返回一個std::io::Stdin的實例,這表明終端標準輸入句柄的類型。而後調用read_line方法,能夠從標準輸入中讀取一行並存入到guess變量中去。&表示這是一個引用,這是一個複雜的特性,咱們如今無須瞭解它。

讀取用戶輸入後,咱們須要判斷用戶是否正確輸入了數字。String類型帶有處理字符串處理的一些方法:

let guess: u32 = guess.trim().parse().expect("Please type a number!");

字符串的 parse 方法將字符串解析成數字。由於這個方法能夠解析多種數字類型,所以須要告訴 Rust 具體的數字類型,這裏經過 let guess: u32 指定。guess 後的冒號:告訴 Rust 咱們指定了變量的類型。Rust 有一些內建的數字類型,u32 是一個無符號的 32 位整型。trim方法用於消除回車空格等符號。

錯誤處理

上一小節代碼中還有一個expect沒有分析,而這就涉及到Rust中的錯誤處理機制了。read_line的返回值類型是io::Result,它是Result類型在io模塊的特化版本。Result是枚舉類型,其成員爲OkErrOk 成員表示操做成功,內部包含成功時產生的值。Err 成員則意味着操做失敗,而且包含失敗的來龍去脈。

Result類型的做用是編碼錯誤處理信息。Result 類型像其餘類型同樣,擁有定義於其上的方法。io::Result的實例擁有expect方法。若是 io::Result 實例的值是 Errexpect 會致使程序崩潰,並打印參數傳遞給 expect 的信息。若是io::Result實例的值是 Okexpect 會獲取 Ok 中的值並返回。在本例中,這個值是用戶輸入到標準輸入中的字節數。

隨機數生成

猜謎遊戲須要可以自動生成隨機數。Rust標準庫中還沒有包含隨機數功能,但咱們能夠經過引入外部crate來得到隨機數功能。還記得Rust的官方開源倉庫嗎,那裏但是有不少寶貝的。打開https://crates.io/,在搜索框中鍵入rand來搜索具備隨機數功能的crate,出來的第一個結果就是咱們須要的crate。

如今咱們將這個庫引入到咱們的項目中。打開Cargo.toml,在[dependencies]下添加:

[dependencies]

rand = "0.8.3"

[dependencies] 告訴 Cargo 本項目依賴了哪些外部 crate 及其版本。

下面使用rand庫來產生隨機數。首先,使用use語句引入randuse rand::Rng;。而後調用下列函數產生一個1和100之間的數:

let number = rand::thread_rng().gen_range(1..=100);

完整代碼

猜謎遊戲的完整代碼以下。

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..=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;
            }
        }
    }
}

其中,涉及控制流操做的loopmatchcontinuebreak等語法,你們應當可以望文生義。對於這個完整代碼,你們可以閱讀並知道每一行幹了啥便可,沒必要糾結於語法細節。

猜謎遊戲運行結果以下:

參考文獻

  • 《The Rust Programming Language》
  • 《Rust編程之道》張漢東
  • 《深刻淺出Rust》範長春
相關文章
相關標籤/搜索