鍍金池/ 教程/ Java/ 猜猜看
哲學(xué)家就餐問題
鏈接進階
名詞中英文對照
測試
引用和借用
泛型
方法語法
函數(shù)
不安全代碼
并發(fā)
裝箱語法和模式
注釋
棧和堆
運算符與重載
語法索引
文檔
固有功能
所有權(quán)
循環(huán)
通用函數(shù)調(diào)用語法
不定長類型
<code>const</code> 和 <code>static</code>
迭代器
其他語言中的 Rust
枚舉
詞匯表
If語句
猜猜看
錯誤處理
生命周期
編譯器插件
發(fā)布途徑
閉包
trait 對象
不使用標(biāo)準庫
關(guān)聯(lián)常量
外部函數(shù)接口(FFI)
類型轉(zhuǎn)換
原生類型
匹配
參考文獻
Rust 編程語言
內(nèi)聯(lián)匯編
條件編譯
選擇你的保證
學(xué)習(xí) Rust
`type`別名
自定義內(nèi)存分配器
屬性
if let
高效 Rust
可變性
語法和語義
模式
基準測試
結(jié)構(gòu)體
變量綁定
語言項
切片模式
<code>Deref</code> 強制多態(tài)
關(guān)聯(lián)類型
裸指針
<code>Borrow</code> 和 <code>AsRef</code>
準備
Rust 開發(fā)版
字符串

猜猜看

guessing-game.md
commit 6ba952020fbc91bad64be1ea0650bfba52e6aab4

讓我學(xué)習(xí)一些 Rust!作為第一個項目,我們來實現(xiàn)一個經(jīng)典新手編程問題:猜猜看游戲。它是這么工作的:程序?qū)S機生成一個 1 到 100 之間的隨機數(shù)。它接著會提示猜一個數(shù)。當(dāng)我們猜了一個數(shù)之后,它會告訴我們是太大了還是太小了。猜對了,它會祝賀我們。聽起來如何?

準備

讓我們準備一個新項目。進入到項目目錄。還記得曾經(jīng)我們?nèi)绾蝿?chuàng)建hello_world的項目目錄和Cargo.toml文件的嗎?Cargo 有一個命令來為我們做這些。讓我們試試:

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

我們將項目名字傳遞給cargo new,然后用了--bin標(biāo)記,因為要創(chuàng)建一個二進制文件,而不是一個庫文件。

查看生成的Cargo.toml文件:

[package]

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

Cargo 從系統(tǒng)環(huán)境變量中獲取這些信息。如果這不對,趕緊修改它。

最后,Cargo 為我們生成了一個“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文件。我們會將所有代碼寫在這個文件里。稍后我們會講到多文件項目。

在我們繼續(xù)之前,讓我們再告訴你一個新的 Cargo 命令:runcargo runcargo build類似,并且還會運行我們剛生成的可執(zhí)行文件。試試它:

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

很好!run命令在我們需要快速重復(fù)運行一個項目時非常方便。我們的游戲就是這么一個項目,在我們添加新內(nèi)容之前我們需要經(jīng)??焖贉y試項目。

處理一次猜測

讓我們開始吧!我們需要做的第一件事是讓我們的玩家輸入一個猜測。把這些放入你的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);
}

這有好多東西!讓我們一點一點地過一遍。

use std::io;

我們需要獲取用戶輸入,并接著打印結(jié)果作為輸出。為此,我們需要標(biāo)準庫的io庫。Rust 為所有程序只導(dǎo)入了很少一些東西,‘prelude’。如果它不在預(yù)先導(dǎo)入中,你將不得不直接use它。這還有第二個"prelude",ioprelude,它也起到了類似的作用:你引入它,它引入一系列擁有的 IO 相關(guān)的庫。

fn main() {

就像你之前見過的,main()是你程序的入口點。fn語法聲明了一個新函數(shù),()表明這里沒有參數(shù),而{開始了函數(shù)體。因為不包含返回類型,它假設(shè)是(),一個空的[元組](5.3.Primitive Types 原生類型.md#tuples)。

    println!("Guess the number!");

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

我們之前學(xué)過println!()是一個在屏幕上打印[字符串](5.17.Strings 字符串.md)的[宏](5.34.Macros 宏.md)。

    let mut guess = String::new();

現(xiàn)在我們遇到有意思的東西了!這一小行有很多內(nèi)容。第一個我們需要注意到的是[let語句](5.1.Variable Bindings 變量綁定.md),它用來創(chuàng)建“變量綁定”。它使用這個形式:

let foo = bar;

這會創(chuàng)建一個叫做foo的新綁定,并綁定它到bar這個值上。在很多語言中,這叫做一個“變量",不過 Rust 的變量綁定暗藏玄機。

例如,它們默認是[不可變的](5.10.Mutability 可變性.md)。這時為什么我們的例子使用了mut:它讓一個綁定可變,而不是不可變。let并不從左手邊獲取一個名字,事實上它接受一個[模式(pattern)](5.14.Patterns 模式.md)。我們會在后面更多的使用模式?,F(xiàn)在它使用起來非常簡單:

let foo = 5; // immutable.
let mut bar = 5; // mutable

噢,同時//會開始一個注釋,直到這行的末尾。Rust 忽略[注釋](5.4.Comments 注釋.md)中的任何內(nèi)容。

那么現(xiàn)在我們知道了let mut guess會引入一個叫做guess的可變綁定,不過我們也必須看看=的右側(cè)所綁定的內(nèi)容:String::new()。

String是一個字符串類型,由標(biāo)準庫提供。[String](5.17.Strings 字符串.md)是一個可增長的,UTF-8編碼的文本。

::new()語法用了::因為它是一個特定類型的”關(guān)聯(lián)函數(shù)“。這就是說,它與String自身關(guān)聯(lián),而不是與一個特定的String實例關(guān)聯(lián)。一些語言管這叫一個“靜態(tài)方法”。

這個函數(shù)叫做new(),因為它創(chuàng)建了一個新的,空的String。你會在很多類型上找到new()函數(shù),因為它是創(chuàng)建一些類型新值的通常名稱。

讓我們繼續(xù):

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

這稍微有點多!讓我們一點一點來。第一行有兩部分。這是第一部分:

io::stdin()

還記得我們?nèi)绾卧诔绦虻牡谝恍?code>use std::io的嗎?現(xiàn)在我們調(diào)用了一個與之相關(guān)的函數(shù)。如果我們不use std::io,那么我們就得寫成std::io::stdin()

這個特殊的函數(shù)返回一個指向你終端標(biāo)準輸入的句柄。更具體的,可參考std::io::Stdin。

下一部分將用這個句柄去獲取用戶輸入:

.read_line(&mut guess)

這里,我們對我們的句柄調(diào)用了read_line()方法。[“方法”](5.15.Method Syntax 方法語法.md)就像關(guān)聯(lián)函數(shù),不過只在一個類型的特定實例上可用,而不是這個類型本身。我們也向read_line()傳遞了一個參數(shù):&mut guess。

還記得我們上面怎么綁定guess的嗎?我們說它是可變的。然而,read_line并不接收String作為一個參數(shù):它接收一個&mut String。Rust有一個叫做[“引用”](5.8.References and Borrowing 引用和借用.md)的功能,它允許你對一片數(shù)據(jù)有多個引用,用它可以減少拷貝。引用是一個復(fù)雜的功能,因為Rust的一個主要賣點就是它如何安全和便捷地使用引用。然而,目前我們還不需要知道很多細節(jié)來完成我們的程序?,F(xiàn)在,所有我們需要了解的是像let綁定,引用默認是不可變的。因此,我們需要寫成&mut guess,而不是&guess。

為什么read_line()會需要一個字符串的可變引用呢?它的工作是從標(biāo)準輸入獲取用戶輸入,并把它放入一個字符串。所以它用字符串作為參數(shù),為了可以增加輸入,它必須是可變的。

不過我們還未完全看完這行代碼。雖然它是單獨的一行代碼,但只是這個單獨邏輯代碼行的開頭部分:

        .expect("Failed to read line");

當(dāng)你用.foo()語法調(diào)用一個函數(shù)的時候,你可能會引入一個新行符或其它空白。這幫助我們拆分長的行。我們可以這么干:

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

不過這樣會難以閱讀。所以我們把它分開,3 行對應(yīng) 3 個方法調(diào)用。我們已經(jīng)談?wù)撨^了read_line(),不過expect()呢?好吧,我們已經(jīng)提到過read_line()將用戶輸入放入我們傳遞給它的&mut String中。不過它也返回一個值:在這個例子中,一個io::Result。Rust的標(biāo)準庫中有很多叫做Result的類型:一個泛型Result,然后是子庫的特殊版本,例如io::Result。

這個Result類型的作用是編碼錯誤處理信息。Result類型的值,像任何(其它)類型,有定義在其上的方法。在這個例子中,io::Result有一個expect()方法獲取調(diào)用它的值,而且如果它不是一個成功的值,[panic!](Error Handling 錯誤處理.md)并帶有你傳遞給它的信息。這樣的panic!會使我們的程序崩潰,顯示(我們傳遞的)信息。

如果我們?nèi)サ暨@兩個函數(shù)調(diào)用,我們的程序會編譯通過,不過我們會得到一個警告:

$ 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的值。這個警告來自io::Result的一個特殊注解。Rust 嘗試告訴你你并未處理一個可能的錯誤。阻止錯誤的正確方法是老實編寫錯誤處理。幸運的是,如果我們只是想如果這有一個問題就崩潰的話,我們可以用這兩個小方法。如果我們想從錯誤中恢復(fù)什么的,我們得做點別的,不過我們會把它留給接下來的項目。

這是我們第一個例子僅剩的一行:

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

這打印出我們保存輸入的字符串。{}是一個占位符,所以我們傳遞guess作為一個參數(shù)。如果我們有多個{},我們應(yīng)該傳遞多個參數(shù):

let x = 5;
let y = 10;

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

簡單加愉快。

總而言之,這只是一個觀光。我們可以用cargo run運行我們寫的:

$ 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

好的!我們的第一部分完成了:我們可以從鍵盤獲取輸入,并把它打印回去。

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

接下來,我們要生成一個秘密數(shù)字。Rust標(biāo)準庫中還未包含隨機數(shù)功能。然而,Rust 團隊確實提供了一個rand crate。一個“包裝箱”(crate)是一個 Rust 代碼的包。我們已經(jīng)構(gòu)建了一個”二進制包裝箱“,它是一個可執(zhí)行文件。rand是一個”庫包裝箱“,它包含被認為應(yīng)該被其它程序使用的代碼。

使用外部包裝箱是 Cargo 的亮點。在我們使用rand編寫代碼之前,我們需要修改我們的Cargo.toml。打開它,并在末尾增加這幾行:

[dependencies]

rand="0.3.0"

Cargo.toml[dependencies]部分就像[package]部分:所有之后的東西都是它的一部分,直到下一個部分開始。Cargo使用依賴部分來知曉你用的外部包裝箱的依賴,和你要求的版本。在這個例子中,我們用了0.3.0版本。Cargo理解語義化版本,它是一個編寫版本號的標(biāo)準。如果我們就是只想使用0.3.0,我們可以用=0.3.0。如果我們想要使用最新版本我們可以使用*或者我們可以使用一個范圍的版本。Cargo文檔包含更多細節(jié)。

現(xiàn)在,在不修改任何我們代碼的情況下,讓我們構(gòu)建我們的項目:

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

(當(dāng)然,你可能會看到不同的版本)

很多新的輸出!現(xiàn)在我們有了一個外部依賴,Cargo 從記錄中獲取了所有東西的最新版本,它們是來自Crates.io的一份拷貝。Crates.io 是 Rust 生態(tài)系統(tǒng)中人們發(fā)表開源 Rust 項目供他人使用的地方。

在更新了記錄后,Cargo 檢查我們的[dependencies]并下載任何我們還沒有的東西。在這個例子中,雖然我們只說了我們要依賴rand,我們也獲取了一份libc的拷貝。這是因為rand依賴libc工作。在下載了它們之后,它編譯它們,然后接著編譯我們的項目。

如果我們再次運行cargo build,我們會得到不同的輸出:

$ cargo build

沒錯,沒有輸出!Cargo 知道我們的項目被構(gòu)建了,并且所有它的依賴也被構(gòu)建了,所以沒有理由再做一遍所有這些。沒有事情做,它簡單地退出了。如果我們再打開src/main.rs,做一個無所謂的修改,然后接著再保存,我們就會看到一行:

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

所以,我們告訴Cargo我們需要任何0.3.x版本的rand,并且因此它獲取在本文被編寫時的最新版,v0.3.8。不過你瞧瞧當(dāng)下一周,v0.3.9出來了,帶有一個重要的 bug 修改嗎?雖然 bug 修改很重要,不過如果0.3.9版本包含破壞我們代碼的回歸呢?

這個問題的回答是現(xiàn)在你會在你項目目錄中找到的Cargo.lock。當(dāng)你第一次構(gòu)建你的項目的時候,Cargo 查明所有符合你的要求的版本,并接著把它們寫到了Cargo.lock文件里。當(dāng)你在未來構(gòu)建你的項目的時候,Cargo 會注意到Cargo.lock的存在,并接著使用指定的版本而不是再次去做查明版本的所有工作。這讓你有了一個可重復(fù)的自動構(gòu)建。換句話說,我們會保持在0.3.8直到我們顯式的升級,這對任何使用我們共享的代碼的人同樣有效,感謝鎖文件。

當(dāng)我們確實想要使用v0.3.9怎么辦?Cargo 有另一個命令,update,它代表“忽略鎖,搞清楚所有我們指定的最新版本。如果這能工作,將這些版本寫入鎖文件”。不過,默認,Cargo 只會尋找大于0.3.0小于0.4.0的版本。如果你想要移動到0.4.x,我們不得不直接更新Cargo.toml文件。當(dāng)我們這么做,下一次我們cargo build,Cargo會更新索引并重新計算我們的rand要求。

關(guān)于Cargo它的生態(tài)系統(tǒng)有很多東西要說,不過眼下,這是我們需要知道的一切。Cargo讓重用庫變得真正的簡單,并且Rustacean們可以編寫更小的由很多子包組裝成的項目。

讓我們真正的使用rand,這是我們的下一步:

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)
        .expect("failed to read line");

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

先修改第一行。現(xiàn)在它是extern crate rand。因為在[dependencies]聲明了rand,我們可以用extern crate來讓Rust知道我們正在使用它。這也等同于一個use rand;,所以我們可以通過rand::前綴使用rand包裝箱中的一切。

下一步,我們增加了另一行useuse rand::Rng。我們一會將要使用一個方法,并且它要求Rng在作用域中才能工作。這個基本觀點是:方法定義在一些叫做“特性(traits,也有譯作特質(zhì))”的東西上面,而為了讓方法能夠工作,需要這個特性位于作用域中。關(guān)于更多細節(jié),閱讀trait部分。

這里還有兩行我們增加的,在中間:

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

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

我們用rand::thread_rng()函數(shù)來獲取一個隨機數(shù)生成器的拷貝,它位于我們特定的執(zhí)行[線程](4.6.Concurrency 并發(fā).md)的本地。因為use rand::Rng了,有一個gen_range()方法可用。這個函數(shù)獲取兩個參數(shù),并產(chǎn)生一個位于其間的數(shù)字。它包含下限,不過不包含上限,所以需要1101來生成一個1100之間的數(shù)。

第二行僅僅打印出了秘密數(shù)字,這在開發(fā)程序時做簡單測試很有用。不過在最終版本中我們會刪除它。在開始就打印出結(jié)果就沒什么可玩的了!

嘗試運行新程序幾次:

$ 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

好的!接下來:讓我們比較我們的猜測和秘密數(shù)字。

比較猜測

現(xià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)
        .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!"),
    }
}

這有一些新東西。第一個是另一個use。我們帶來了一個叫做std::cmp::Ordering類型到作用域中。接著,底部5行代碼使用了它:

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

cmp()可以在任何能被比較的值上調(diào)用,并且它獲取你想要比較的值的引用。它返回我們之前useOrdering類型。我們使用一個[match](5.13.Match 匹配.md)語句來決定具體是哪種OrderingOrdering是一個[枚舉(enum)](5.12.Enums 枚舉.md),它看起來像這樣:

enum Foo {
    Bar,
    Baz,
}

通過這個定義,任何Foo可以是Foo::Bar或者Foo::Baz。我們用::來表明一個特定enum變量的命名空間。

Ordering枚舉有3個可能的變量:Less,EqualGreater。match語句獲取類型的值,并讓你為每個可能的值創(chuàng)建一個“分支”。因為有 3 種類型的Ordering,我們有 3 個分支:

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!,而如果EqualYou win!match真的非常有用,并且在 Rust 中經(jīng)常使用。

我確實提到過我們還不能正常運行,雖然。讓我們試試:

$ 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`.

噢!這是一個大錯誤。它的核心是我們有“不匹配的類型”。Rust 有一個強大的靜態(tài)類型系統(tǒng)。然而,它也有類型推斷。當(dāng)我們寫let guess = String::new(),Rust能夠推斷出guess應(yīng)該是一個String,并因此不需要我們寫出類型。而我們的secret_number,這有很多類型可以有從1100的值:i32,一個 32 位數(shù),或者u32,一個無符號的32位值,或者i64,一個 64 位值?;蛘咂渌裁吹?。目前為止,這并不重要,所以 Rust 默認為i32。然而,這里,Rust 并不知道如何比較guesssecret_number。它們必須是相同的類型。最終,我們想要我們作為輸入讀到的String轉(zhuǎn)換為一個真正的數(shù)字類型,來進行比較。我們可以用額外 3 行來搞定它。這是我們的新程序:

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)
        .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!"),
    }
}

新的 3 行是:

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

稍等,我認為我們已經(jīng)用過了一個guess?確實,不過 Rust 允許我們用新值“遮蓋(shadow)”之前的guess。這在這種具體的情況中經(jīng)常被用到,guess開始是一個String,不過我們想要把它轉(zhuǎn)換為一個u32。遮蓋(Shadowing)讓我們重用guess名字,而不是強迫我們想出兩個獨特的像guess_strguess,或者別的什么。

我們綁定guess到一個看起來像我們之前寫的表達式:

guess.trim().parse()

這里,guess引用舊的guess,那個我們輸入用到的String。Stringtrim()方法會去掉我們字符串開頭和結(jié)尾的任何空格。這很重要,因為我們不得不按“回車”鍵來滿足read_line()。這意味著如果輸入5并按回車,guess看起來像這樣:5\n\n代表“新行”,回車鍵。trim()去掉這些,保留5給我們的字符串。字符串的parse()方法將字符串解析為一些類型的數(shù)字。因為它可以解析多種數(shù)字,我們需要給Rust一些提醒作為我們具體想要的數(shù)字的類型。因此,let guess: u32。guess后面的分號(:)告訴 Rust 我們要標(biāo)注它的類型。u32是一個無符號的,32位整型。Rust 有[一系列內(nèi)建數(shù)字類型](Primitive Types 原生類型.md#數(shù)字類型),不過我們選擇了u32。它是一個小正數(shù)的默認好選擇。

就像read_line(),我們調(diào)用parse()可能產(chǎn)生一個錯誤。如果我們的字符串包含A

上一篇:枚舉下一篇:切片模式