天天看點

rust學習筆記1.0 rustup2.0 cargo3.0 一個小項目4.0 rust程式設計基本概念5.0 結構體6.0枚舉和模式比對7.0 包,crate ,項目管理8.0 常見集合9.0 錯誤處理

文章目錄

  • 1.0 rustup
    • 1.1 簡介
    • 1.2 安裝
  • 2.0 cargo
    • 2.1 簡介
    • 2.2 安裝
    • 2.3 建構項目
  • 3.0 一個小項目
    • 3.1添加依賴crate
    • 3.2 match分支
    • 3.3 循環
  • 4.0 rust程式設計基本概念
    • 4.1 變量的可變性
    • 4.2 常量變量差別
    • 4.3 隐藏
    • 4.4 資料類型
      • 4.4.1 标量(scalar)
      • 4.4.2 複合(compound)
    • 4.5 函數工作流程
      • 4.5.1 函數參數
      • 4.5.2 具有傳回值的函數
    • 4.6 控制流
      • 4.6.1 if表達式
      • 4.6.2 循環
    • 4.7 所有權
      • 4.7.1 堆和棧
      • 4.7.2 rust所有權的規則
  • 5.0 結構體
    • 5.1方法使用
      • 5.2.1帶有更多參數的方法
    • 5.3 關聯函數
  • 6.0枚舉和模式比對
    • 6.1 Option枚舉
    • 6.2 match控制流
    • 6.3比對Option< T>
    • 6.4 if let控制流
  • 7.0 包,crate ,項目管理
    • 7.1 包和crate
    • 7.2 定義子產品來控制作用域的私有性
    • 7.3引用子產品樹的路徑
    • 7.4 使用use将名稱引入作用域
    • 7.5 将子產品放入不同的檔案内
  • 8.0 常見集合
    • 8.1 vector向量
    • 8.2 字元串
    • 8.3 hashmap
  • 9.0 錯誤處理
    • 9.1 panic! 與不可恢複的錯誤
    • 9.2 Result 與可恢複的錯誤

1.0 rustup

1.1 簡介

rustup是一個管理 Rust 版本和相關工具的指令行工具。下載下傳時需要聯網。

1.2 安裝

我們使用的系統是debian8

首先我們檢視遠端的腳本并且運作

[email protected]:~# curl  https://sh.rustup.rs | sh 
           

選擇1安裝

Current installation options:


$<2>   $<2>default host triple: $<2>x86_64-unknown-linux-gnu$<2>
$<2>     $<2>default toolchain: $<2>stable$<2>
$<2>               $<2>profile: $<2>default$<2>
  modify PATH variable: $<2>yes$<2>

1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
>1
>$<2>info: $<2>profile set to 'default'
$<2>info: $<2>syncing channel updates for 'stable-x86_64-unknown-linux-gnu'
$<2>info: $<2>latest update on 2019-11-07, rust version 1.39.0 (4560ea788 2019-11-04)
$<2>info: $<2>downloading component 'cargo'
$<2>info: $<2>downloading component 'clippy'
$<2>info: $<2>downloading component 'rust-docs'
$<2>info: $<2>downloading component 'rust-std'
176.4 MiB / 176.4 MiB (100 %)   9.2 MiB/s in 22s ETA:  0s
$<2>info: $<2>downloading component 'rustc'
 66.3 MiB /  66.3 MiB (100 %)   8.8 MiB/s in  8s ETA:  0s
$<2>info: $<2>downloading component 'rustfmt'
$<2>info: $<2>installing component 'cargo'
$<2>info: $<2>installing component 'clippy'
$<2>info: $<2>installing component 'rust-docs'
 11.8 MiB /  11.8 MiB (100 %) 881.6 KiB/s in 12s ETA:  0s
$<2>info: $<2>installing component 'rust-std'
176.4 MiB / 176.4 MiB (100 %)   8.3 MiB/s in 28s ETA:  0s
  3 iops /   3 iops (100 %)   1 iops/s in  2s ETA:  0s    
$<2>info: $<2>installing component 'rustc'
 66.3 MiB /  66.3 MiB (100 %)   6.7 MiB/s in 21s ETA:  0s
  6 iops /   6 iops (100 %)   5 iops/s in  1s ETA:  0s    
$<2>info: $<2>installing component 'rustfmt'
$<2>info: $<2>default toolchain set to 'stable'

  $<2>stable installed$<2> - rustc 1.39.0 (4560ea788 2019-11-04)

$<2>
Rust is installed now. Great!
$<2>
To get started you need Cargo's bin directory ($HOME/.cargo/bin) in your $<2>PATH$<2>
environment variable. Next time you log in this will be done
automatically.

To configure your current shell run $<2>source $HOME/.cargo/env$<2>
           

2.0 cargo

2.1 簡介

Cargo 是 Rust 的建構系統和包管理器,cargo可以處理很多任務,比如建構代碼,下載下傳依賴庫,并且編譯這個庫,如果我們寫一個最簡單的hello world程式

fn main() {
	printfln!("hello world!");
}
           

将隻會用到 Cargo 建構代碼的那部分功能。在編寫更複雜的 Rust 程式時,你将添加依賴項,如果使用 Cargo 啟動項目,則添加依賴項将更容易。

2.2 安裝

在我們安裝rustup的時候cargo已經被順帶安裝

2.3 建構項目

我們嘗試使用cargo建構項目

[email protected]:~/rust/project# cargo new hello_cargo_project   #建立一個project
           

我們輸完上述指令後會在此目錄下建立一個hello_cargo_project的目錄 ,我們進入此目錄看一下

[email protected]:~/rust/project# cd hello_cargo_project/ 
[email protected]:~/rust/project/hello_cargo_project# ls -al
total 24
drwxr-xr-x 4 root root 4096 Dec  3 10:00 .
drwxr-xr-x 3 root root 4096 Dec  3 10:00 ..
-rw-r--r-- 1 root root  207 Dec  3 10:00 Cargo.toml
drwxr-xr-x 6 root root 4096 Dec  3 10:00 .git
-rw-r--r-- 1 root root   19 Dec  3 10:00 .gitignore
drwxr-xr-x 2 root root 4096 Dec  3 10:00 src
           

此時發現有git初始檔案,還有cargo.toml檔案(這是 Cargo 配置檔案的格式,使用的是Tom’s Obvious, Minimal Language)格式),src目錄是放我們源碼的目錄,我們先看cargo的配置檔案cargo.homl

[email protected]:~/rust/project/hello_cargo_project# cat Cargo.toml 
[package]
name = "hello_cargo_project"
version = "0.1.0"
authors = ["root"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
[email protected]:~/rust/project/hello_cargo_project# 
           

第一行,[package],是一個片段(section)标題,表明下面的語句用來配置一個包。随着我們在這個檔案增加更多的資訊,還将增加其他片段(section)。接下來的四行設定了 Cargo 編譯程式所需的配置:項目的名稱、版本、作者以及要使用的 Rust 版本。Cargo 從環境中擷取你的名字和 email 資訊,是以如果這些資訊不正确,請修改并儲存此檔案。附錄 E 會介紹 edition 的值。

最後一行,[dependencies],是羅列項目依賴的片段的開始。在 Rust 中,代碼包被稱為 crates。這個項目并不需要其他的 crate(因為就是一個hello world),後面我們其他的項目有可能依賴其他的包會在dependencies下寫相應的配置

此時我們可以再src/下寫一個最簡單的程式hello world

[email protected]:~/rust/project/hello_cargo_project# cd src/
[email protected]:~/rust/project/hello_cargo_project/src# vim.tiny hello.rs 
fn main(){
        println!("hello,world");
}
           

此時cargo為我們生成了一個hello world的程式

Cargo 期望源檔案存放在 src 目錄中。項目根目錄隻存放 README、license 資訊、配置檔案和其他跟代碼無關的檔案。使用 Cargo 幫助你保持項目幹淨整潔,一切井井有條。

開始建構項目

我們輸入以下的指令開始建構cargo項目,在相應的project目錄下,因為他要讀取toml檔案 ,注意一定要裝gcc

[email protected]:~/rust/project/hello_cargo_project# cargo build
           

上述的指令cargo build會建立一個可執行檔案hello_cargo_project(于項目同名),他在PROJECT/targer/debug下 ,我們可以直接執行他列印hello world

[email protected]:~/rust/project/hello_cargo_project/target/debug# ./hello_cargo_project 
Hello, world!
           

首次運作 cargo build 時,也會使 Cargo 在項目根目錄建立一個新檔案:Cargo.lock。這個檔案記錄項目依賴的實際版本。這個項目并沒有依賴,是以其内容比較少。你自己永遠也不需要碰這個檔案,讓 Cargo 處理它就行了。

我們剛剛使用 cargo build 建構了項目,并使用 ./target/debug/hello_cargo 運作了程式,也可以使用 cargo run 在一個指令中同時編譯并運作生成的可執行檔案:

[email protected]:~/rust/project/hello_cargo_project# cargo run 
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/hello_cargo_project`
Hello, world!
[email protected]:~/rust/project/hello_cargo_project# 
           

如果修改了源檔案的話,Cargo 會在運作之前重新建構項目,并會出現像這樣的輸出

Cargo 還提供了一個叫 cargo check 的指令。該指令快速檢查代碼確定其可以編譯,但并不産生可執行檔案:

[email protected]:~/rust/project/hello_cargo_project# cargo check 
    Checking hello_cargo_project v0.1.0 (/root/rust/project/hello_cargo_project)
    Finished dev [unoptimized + debuginfo] target(s) in 0.40s
           

通常 cargo check 要比 cargo build 快得多,因為它省略了生成可執行檔案的步驟。如果你在編寫代碼時持續的進行檢查,cargo check 會加速開發!為此很多 Rustaceans 編寫代碼時定期運作 cargo check 確定它們可以編譯。當準備好使用可執行檔案時才運作 cargo build。

當項目最終準備好釋出時,可以使用 cargo build --release 來優化編譯項目。這會在 target/release 而不是 target/debug 下生成可執行檔案。這些優化可以讓 Rust 代碼運作的更快,不過啟用這些優化也需要消耗更長的編譯時間。這也就是為什麼會有兩種不同的配置:一種是為了開發,你需要經常快速重新建構;另一種是為使用者建構最終程式,它們不會經常重新建構,并且希望程式運作得越快越好。如果你在測試代碼的運作時間,請確定運作 cargo build --release 并使用 target/release 下的可執行檔案進行測試。

cargo文檔手冊如下

cargo 文檔

3.0 一個小項目

我們建立一個項目叫做猜字謎

[email protected]:~/rust/project#cargo new guessing_game
[email protected]:~/rust/project#guessing_game/src
           

在該項目的src下添加mian.rs源代碼,源檔案如下

use std::io;
fn main() {
        println!("guess the number!");
        println!("please input your guess~");
        let mut guess = String::new(); //必須要加上後面的new初始化

        io::stdin().read_line(&mut guess).expect("failed to read line");
        println!("you guessed: {}",guess);
}
           

我們來解讀以上代碼

首先是use std::io

這個風格和c++很想,這段代碼的意思就是将IO庫引入到目前作用域,而IO庫來自于std庫

預設情況下,Rust 将 prelude 子產品中少量的類型引入到每個程式的作用域中。如果需要的類型不在 prelude 中,你必須使用 use 語句顯式地将其引入作用域。std::io 庫提供很多有用的功能,包括接收使用者輸入的功能。

fn main() {

這就是聲明主調函數,第一個執行的函數就是main主調函數,fn 文法聲明了一個新函數,() 表明沒有參數,{ 作為函數體的開始

let mut guess = String::new();

let 語句,用來建立 變量,但是我們一般建立變量像就是這樣let foo = 5;一個叫做 foo 的變量并把它綁定到常量 5 上,并且foo不可變,而我們之前加上mut就是讓guess這個變量可變。我們上述的代碼綁定的值是String::new 的結果,這個函數會傳回一個 String 的新執行個體。String 是一個标準庫提供的字元串類型,它是 UTF-8 編碼的可增長文本塊。

::new 那一行的 :: 文法表明 new 是 String 類型的一個 關聯函數,關聯函數是針對類型實作的,在這個例子中是 String,而不是 String 的某個特定執行個體

new 函數建立了一個新的空字元串,你會發現很多類型上有 new 函數,因為它是建立類型執行個體的慣用函數名。

總結一下,let mut guess = String::new(); 這一行建立了一個可變變量,目前它綁定到一個新的 String 空執行個體上。

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

如果程式的開頭沒有 use std::io 這一行,可以把函數調用寫成 std::io::stdin。stdin 函數傳回一個 std::io::Stdin 的執行個體,這代表終端标準輸入句柄的類型。

代碼的下一部分,.read_line(&mut guess),調用 read_line 方法從标準輸入句柄擷取使用者輸入。我們還向 read_line() 傳遞了一個參數:&mut guess。

read_line 的工作是,無論使用者在标準輸入中鍵入什麼内容,都将其存入一個字元串中,是以它需要字元串作為參數。這個字元串參數應該是可變的,以便 read_line 将使用者輸入附加上去。

& 表示這個參數是一個 引用(reference),它允許多處代碼通路同一處資料,而無需在記憶體中多次拷貝。引用是一個複雜的特性,Rust 的一個主要優勢就是安全而簡單的操縱引用。完成目前程式并不需要了解如此多細節。現在,我們隻需知道它像變量一樣,預設是不可變的。是以,需要寫成 &mut guess 來使其可變,而不是 &guess。

上面read_line()方法傳回一個類型叫做Result,這個類型是枚舉,裡面的成員就是Ok,Err ok代表成功,err代表錯誤,而expect是io::result的方法,如果io::result傳回的結果為err那麼expect會導緻程式崩潰,并顯示當做參數傳遞給 expect 的資訊,如果 io::Result 執行個體的值是 Ok,expect 會擷取 Ok 中的值并原樣傳回。在本例中,這個值是使用者輸入到标準輸入中的位元組數。

然後我們輸入cargo run執行這個項目

[email protected]:~/rust/project/guessing_game# cargo run 
   Compiling guessing_game v0.1.0 (/root/rust/project/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.57s
     Running `target/debug/guessing_game`
guess the number!
please input your guess~
1
you guessed: 1
           

3.1添加依賴crate

我們需要生産一個随機數,我們可以添加一個crate來解決,這個crate就是rend,rend專門用來生成随機數,此時我們在項目的Cargo.toml的[dependencies]中加上rend這個create

[dependencies]
rend = "0.5.5"
           

0.5.5就是rend的版本号,在我們build的時候Cargo 會先跟新 registry,然後從其 上擷取dependencies下crate的包,并且cargo會自己去解決依賴,比如我們rend就依賴libc等包,cargo都會自己幫你下好,這個倉庫就是Crates.io,Crates.io 是 Rust 生态環境中的開發者們向他人貢獻 Rust 開源項目的地方,現在我們在src的main.rs下添加幾行代碼

use std::io;
use rand::Rng;
fn main() {
        println!("guess the number!");
        let secret_number = rand::thread_rng().gen_range(1,101);
        println!("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);
}
           

首先use rand::Rng;

Rng 是一個 trait,它定義了随機數生成器應實作的方法,想使用這些方法的話,此 trait 必須在作用域中

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

rand::thread_rng 函數提供實際使用的随機數生成器:它位于目前執行線程的本地環境中,并從作業系統擷取 seed,接下來,調用随機數生成器的 gen_range 方法。這個方法由剛才引入到作用域的 Rng trait 定義gen_range 方法擷取兩個數字作為參數,并生成一個範圍在兩者之間的随機數。它包含下限但不包含上限,是以需要指定 1 和 101 來請求一個 1 和 100 之間的數。

然後我們再build

3.2 match分支

此時我們再加幾行,

use std::cmp::Ordering;
let guess:u32 = guess.trim().parse().expect("please type a number!");

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

           

首先我們得明白我們之前io::stdin::read_line(&mut guess).expect(“faile…”);輸入的是一個字元,是以我們let guess:u32 = guess.trim().parse().expect(“please type a number!”);就是将guess轉換成u32位的數字(u代表unsigned無符号,32代表32位,i32代表有符号32位),

這裡建立了一個叫做 guess 的變量。不過等等,不是已經有了一個叫做 guess 的變量了嗎?确實如此,不過 Rust 允許用一個新值來 隐藏 (shadow) guess 之前的值。這個功能常用在需要轉換值類型之類的場景。它允許我們複用 guess 變量的名字,而不是被迫建立兩個不同變量

我們将 guess 綁定到 guess.trim().parse() 表達式上。表達式中的 guess 是包含輸入的原始 String 類型。String 執行個體的 trim 方法會去除字元串開頭和結尾的空白字元還有換行,因為我們之前輸入的時候敲了回車,所有有一個\n,

字元串的 parse 方法 将字元串解析成數字。因為這個方法可以解析多種數字類型,是以需要告訴 Rust 具體的數字類型,這裡通過 let guess: u32 指定。guess 後面的冒号(:)告訴 Rust 我們指定了變量的類型。

一個 match 表達式由 分支(arms) 構成。一個分支包含一個 模式(pattern)和表達式開頭的值與分支模式相比對時應該執行的代碼。Rust 擷取提供給 match 的值并挨個檢查每個分支的模式。match 結構和模式是 Rust 中強大的功能,它展現了代碼可能遇到的多種情形,并幫助你確定沒有遺漏處理

讓我們看看使用 match 表達式的例子。假設使用者猜了 50,這時随機生成的秘密數字是 38。比較 50 與 38 時,因為 50 比 38 要大,cmp 方法會傳回 Ordering::Greater。Ordering::Greater 是 match 表達式得到的值。它檢查第一個分支的模式,Ordering::Less 與 Ordering::Greater并不比對,是以它忽略了這個分支的代碼并來到下一個分支。下一個分支的模式是 Ordering::Greater,正确 比對!這個分支關聯的代碼被執行,在螢幕列印出 Too big!。match 表達式就此終止,因為該場景下沒有檢查最後一個分支的必要。

3.3 循環

loop關鍵字可以建立一個無限循環可以讓使用者無限的輸入

此時我們完善我們的程式,讓其可以讀取使用者的錯誤輸入,并且不報錯推出隻是推出目前循環,重新輸入

use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main() {
        println!("guess the number!");
        let secret_number = rand::thread_rng().gen_range(1,101);
        println!("secret_number is {}",secret_number);
        loop{

                let mut guess = String::new();
                println!("please input your guess~");
                io::stdin().read_line(&mut guess).expect("input error");
                let guess:u32 = match guess.trim().parse(){
                        Ok(num) => num,
                        Err(_) => continue,
                };
                println!{"your guest is {} ",guess};
                match guess.cmp(&secret_number){
                        Ordering::Less => println!("Too small"),
                        Ordering::Greater => println!("Too big"),
                        Ordering::Equal => {
                                println!("equal,you win!");
                                break;
                                }
                }
        }
}
           

這些代碼其他還好隻是有一些需要注意

第13行的let就是在把字元串數字轉換成u32數字的時候加一個match,如果match後面轉換操作錯我,就到第二行err,将錯誤賦予_,這個_是一個通配符,這裡代表所有錯誤,然後将執行後面的continue推出此次循環執行下一次循環,而如果轉換正确,那麼就執行第一個分支ok,并且将轉換後結果賦予OK後面的num(轉換如果是ok都會傳回一個返還後的結果),然後執行num,也就是将num賦給guess,

最後的match如果相等就和最後一個分支的條件Ordering::Equal一樣,就會跳轉到這個分支,列印成功,并且break跳出死循環,

總結,我們在match的時候後面式子的傳回類型最好是枚舉,和我們上面的Result,Ordering類型一樣

4.0 rust程式設計基本概念

4.1 變量的可變性

之前我們提到let a = 1;建立并且初始化了一個變量(将1綁定到a上),此變量預設不可變,因為這個是綁定,如果改變了a的值豈不是也改變了1這個常量?,比如以下的代碼就會出錯

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}
           
如果一部分代碼假設一個值永遠也不會改變,而另一部分代碼改變了這個值,第一部分代碼就有可能以不可預料的方式運作。不得不承認這種 bug 的起因難以跟蹤,尤其是第二部分代碼隻是 有時 會改變值。

我們可以在let後面加上mut來聲明x可以被更改,就是可以解綁,再去由其他的變量綁定到他上面,以下就不會報錯

fn main (){
	let mut a = 1;
	println!{"a is {}",a};
	a = 6;  
	println!{"a is {}",a};
}
           

這樣就不會報錯

除了防止出現 bug 外,還有很多地方需要權衡取舍。例如,使用大型資料結構時,适當地使用可變變量,可能比複制和傳回新配置設定的執行個體更快。對于較小的資料結構,總是建立新執行個體,采用更偏向函數式的程式設計風格,可能會使代碼更易了解,為可讀性而犧牲性能或許是值得的。

4.2 常量變量差別

不允許改變值的變量,可能會使你想起另一個大部分程式設計語言都有的概念:常量(constants)。類似于不可變變量,常量是綁定到一個名稱的不允許改變的值,不過常量與變量還是有一些差別。

  • 首先,不允許對常量使用 mut。常量不光預設不能變,它總是不能變。
  • 聲明常量使用 const 關鍵字而不是 let,并且 必須 注明值的類型。
  • 常量可以在任何作用域中聲明,包括全局作用域,這在一個值需要被很多部分的代碼用到時很有用。
  • 最後一個差別是,常量隻能被設定為常量表達式,而不能是函數調用的結果,或任何其他隻能在運作時計算出的值。

這是一個聲明常量的例子,它的名稱是 MAX_POINTS,值是 100,000。(Rust 常量的命名規範是使用下劃線分隔的大寫字母單詞,并且可以在數字字面值中插入下劃線來提升可讀性):

const MAX_POINTS : u32 = 100_000
           

4.3 隐藏

在一個作用域内我們如果使用let又定義了一個同名的變量,那麼前一個同名變量就被隐藏了,意味着使用這個同名變量其綁定的值是第二個此同名變量,let也可以多次隐藏,比如下方代碼

fn main(){
	let a = 1;
	let a = a+5;  
	let a = a+1;  
	println!{"a is {}",a};
}
           
首先程式将a綁定到1上,接着擷取第一個a的初值加5,再使用let a隐藏上一個a,并且綁定到等号右邊的值(上一個a綁定的值+5),以此類推

隐藏與将變量标記為 mut 是有差別的。當不小心嘗試對變量重新指派時,如果沒有使用 let 關鍵字,就會導緻編譯時錯誤。通過使用 let,我們可以用這個值進行一些計算,不過計算完之後變量仍然是不變的。

mut 與隐藏的另一個差別是,當再次使用 let 時,實際上建立了一個新變量,我們可以改變值的類型,但複用這個名字。

4.4 資料類型

首先我們的rust是靜态類型語言,也就是說在編譯的時候必須知道所有變量的類型根據值及其使用方式,編譯器通常可以推斷出我們想要用的類型。

但是,像我們之前的猜字謎遊戲,使用 parse 将 String 轉換為數字時,必須增加類型注解,像這樣

let guess:u32 = "42".trim().parse().expect("input a number");
           

這裡如果不添加類型注解,Rust 會顯示如下錯誤,這說明編譯器需要我們提供更多資訊,來了解我們想要的類型:

error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".trim().parse().expect("Not a number!");
  |         ^^^^^
  |         |
  |         cannot infer type for `_`
  |         consider giving `guess` a type
           

我們這裡講rust的2類資料類型的子集,标量(scalar)和複合(compound)

4.4.1 标量(scalar)

标量(scalar)類型代表一個單獨的值。Rust 有四種基本的标量類型:整型、浮點型、布爾類型和字元類型。你可能在其他語言中見過它們。讓我們深入了解它們在 Rust 中是如何工作的。

整型

整數 是一個沒有小數部分的數字。我們在第二章使用過 u32 整數類型。該類型聲明表明,它關聯的值應該是一個占據 32 比特位的無符号整數(有符号整數類型以 i 開頭而不是 u)。表格展示了 Rust 内建的整數類型。在有符号列和無符号列中的每一個變體(例如,i16)都可以用來聲明整數值的類型。

長度 有符号 無符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

每一個變體都可以是有符号或無符号的,并有一個明确的大小。有符号 和 無符号 代表數字能否為負值有符号數以補碼形式(two’s complement representation) 存儲。

每一個有符号的變體可以儲存包含從 -[2^(n - 1)] 到 2^(n - 1) - 1 在内的數字,這裡 n 是變體使用的位數。

另外,isize 和 usize 類型依賴運作程式的計算機架構:64 位架構上它們是 64 位的, 32 位架構上它們是 32 位的。

rust預設數字類型是i32

rust也可以通過以下的表格表達各個進制的字面值

數字字面值位數 例子
10進制 98_222
16進制 0xff
8進制 0o77
二進制 0b1111_0000
Byte (u8 only) b’A’

當我們的整型溢出了怎麼辦? 關于這一行為 Rust 有一些有趣的規則。當在 debug 模式編譯時,Rust 檢查這類問題并使程式 panic,這個術語被 Rust 用來表明程式因錯誤而退出。

在 release 建構中,Rust 不檢測溢出,相反會進行一種被稱為二進制補碼包裝(two’s complement wrapping)的操作。簡而言之,256 變成 0,257 變成 1,依此類推。依賴整型溢出被認為是一種錯誤,即便可能出現這種行為。如果你确實需要這種行為,标準庫中有一個類型顯式提供此功能,Wrapping。

浮點型(floating-point numbers)

Rust 也有兩個原生的 浮點數(floating-point numbers)類型,它們是帶小數點的數字。Rust 的浮點數類型是 f32 和 f64,分别占 32 位和 64 位。預設類型是 f64,因為在現代 CPU 中,它與 f32 速度幾乎一樣,不過精度更高。

fn main () {
	let mut a = 2.0;  //f64  
	a:f32 = 3.0;  //f32 
}
           

浮點數采用 IEEE-754 标準表示。f32 是單精度浮點數,f64 是雙精度浮點數。

算數運算

Rust 中的所有數字類型都支援基本數學運算:加法、減法、乘法、除法和取餘。下面的代碼展示了如何在 let 語句中使用它們:

fn main() {
    // 加法
    let sum = 5 + 10;

    // 減法
    let difference = 95.5 - 4.3;

    // 乘法
    let product = 4 * 30;

    // 除法
    let quotient = 56.7 / 32.2;

    // 取餘
    let remainder = 43 % 5;
}
           
bool類型

正如其他大部分程式設計語言一樣,Rust 中的布爾類型有兩個可能的值:true 和 false。Rust 中的布爾類型使用 bool 表示。例如:

fn main () {
	let t = true;  
	let f:bool = false; //顯示的定義bool
}
           

使用布爾值的主要場景是條件表達式,例如 if 表達式類的 “控制流”(“Control Flow”)

字元類型

目前為止隻使用到了數字,不過 Rust 也支援字母。Rust 的 char 類型是語言中最原生的字母類型,如下代碼展示了如何使用它。(注意 char 由單引号指定,不同于字元串使用雙引号。)

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}
           

Rust 的 char 類型的大小為四個位元組(four bytes),并代表了一個 Unicode 标量值(Unicode Scalar Value),這意味着它可以比 ASCII 表示更多内容。在 Rust 中,拼音字母(Accented letters),中文、日文、韓文等字元,emoji(繪文字)以及零長度的空白字元都是有效的 char 值。Unicode 标量值包含從 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF 在内的值。不過,“字元” 并不是一個 Unicode 中的概念,是以人直覺上的 “字元” 可能與 Rust 中的 char 并不符合。第八章的 “使用字元串存儲 UTF-8 編碼的文本” 中将詳細讨論這個主題。

4.4.2 複合(compound)

rust的複合類型中有2個原生的複合類型,元組(tuple),數組(array)

元組(tuple)類型

元組是一個将多個其他類型的值組合進一個複合類型的主要方式。元組長度固定:一旦聲明,其長度不會增大或縮小。

我們使用包含在圓括号中的逗号分隔的值清單來建立一個元組。元組中的每一個位置都有一個類型,而且這些不同值的類型也不必是相同的,但是要加類型的注解,或者不加,表示哪一個元素是什麼類型

fn main (){
	let tup = (500,6.4,1);
	let (x,y,z) = tup;
	println!("y is {}",y);
}
           

程式首先建立了一個元組并綁定到 tup 變量上。接着使用了 let 和一個模式将 tup 分成了三個不同的變量,x、y 和 z。這叫做** 解構(destructuring)**,因為它将一個元組拆成了三個部分。最後,程式列印出了 y 的值,也就是 6.4。

除了使用模式比對解構外,也可以使用點号(.)後跟值的索引來直接通路它們。例如:

fn main () {
	let x:(i32,f64,u8) = (500,6.4,1);
	let five_hundred = x.0; 
	let six_point_four = x.1; 
	let one = x.2;
}
           

上面的例子看到元組的索引也是從0開始

數組(array)類型

另一個包含多個值的方式是 數組(array)。與元組不同,數組中的每個元素的類型必須相同。Rust 中的數組與一些其他語言中的數組不同,因為 Rust 中的數組是固定長度的:一旦聲明,它們的長度不能增長或縮小。

Rust 中,數組中的值位于中括号内的逗号分隔的清單中:

fn main (){
	let a = [1,2,3,4,5];
}
           

當你想要在棧(stack)而不是在堆(heap)上為資料配置設定空間,或者是想要確定總是有固定數量的元素時,數組非常有用。但是數組并不如 vector 類型靈活

vector 類型是标準庫提供的一個 允許 增長和縮小長度的類似數組的集合類型。

當不确定是應該使用數組還是 vector 的時候,你可能應該使用 vector

當程式需要知道一年中月份的名字時。程式不大可能會去增加或減少月份。這時你可以使用數組,因為我們知道它總是包含 12 個元素:

let month = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
           

可以像這樣編寫數組的類型:在方括号中包含每個元素的類型,後跟分号,再後跟數組元素的數量。

let a:[i32;5] = [1,2,3,4,5];
           

這裡,i32 是每個元素的類型。分号之後,數字 5 表明該數組包含五個元素。

這樣編寫數組的類型類似于另一個初始化數組的文法:如果你希望建立一個每個元素都相同的數組,可以在中括号内指定其初始值,後跟分号,再後跟數組的長度,如下所示:

let a = [3;5];  //這樣建立了5個元素,每個元素都是3,3還是i32的,因為i32是預設類型
           

數組是一整塊配置設定在棧上的記憶體

fn main (){
	let a = [1,2,3,4,5];
	let first = a[0];
	let second = a[1];
}
           

當我們通路的數組下标超過了數組元素的個數怎麼辦,

fn main() {
    let a = [1, 2, 3, 4, 5];
    let index = 10;

    let element = a[index];

    println!("The value of element is: {}", element);
}
           

使用 cargo run 運作代碼後會産生如下結果

$ cargo run
   Compiling arrays v0.1.0 (file:///projects/arrays)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/arrays`
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is
 10', src/main.rs:5:19
note: Run with `RUST_BACKTRACE=1` for a backtrace.
           

編譯并沒有産生任何錯誤,不過程式會出現一個 運作時(runtime)錯誤并且不會成功退出。當嘗試用索引通路一個元素時,Rust 會檢查指定的索引是否小于數組的長度。如果索引超出了數組長度,Rust 會 panic,這是 Rust 術語,它用于程式因為錯誤而退出的情況。

這是第一個在實戰中遇到的 Rust 安全原則的例子。在很多底層語言中,并沒有進行這類檢查,這樣當提供了一個不正确的索引時,就會通路無效的記憶體。通過立即退出而不是允許記憶體通路并繼續執行,Rust 讓你避開此類錯誤。第九章會讨論更多 Rust 的錯誤處理。

4.5 函數工作流程

在我們上面寫的代碼中我們用的最多的main函數是很多程式的入口,fn關鍵字代表聲明一個新的函數

Rust 代碼中的函數和變量名使用 snake case 規範風格。在 snake case 中,所有字母都是小寫并使用下劃線分隔單詞。這是一個包含函數定義示例的程式:

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

    another_function();
}

fn another_function() {
    println!("Another function.");
}
           

注意程式中已定義 another_function 函數,是以可以在 main 函數中調用它,源碼中 another_function 定義在 main 函數 之後;也可以定義在之前。Rust 不關心函數定義于何處,隻要定義了就行。

main 函數中的代碼會按順序執行。首先,列印 “Hello, world!” 資訊,然後調用 another_function 函數并列印它的資訊。

4.5.1 函數參數

函數也可以被定義為擁有 參數(parameters),當函數擁有參數(形參)時,可以為這些參數提供具體的值(實參),以下代碼main函數中傳遞了一個實參進去

fn main(){
	another_functions(5);
}
fn another_functions(x:i32){
	println!("x is {}",x);
}
           

another_function 的聲明中有一個命名為 x 的參數。x 的類型被指定為 i32。當将 5 傳給 another_function 時,println! 宏将 5 放入格式化字元串中大括号的位置。

在函數簽名中,必須聲明每個參數的類型。

當一個函數有多個參數時,使用逗号分隔,像這樣

fn main(){
	another_function(5,6);
}
fn another_function(x:i32,y:i32){
	println!("x is {}",x);
	println!("y is {}",y);
}
           

我們知道函數是由一系列語句和一個可選的結尾表達式構成,目前為止,我們隻介紹了沒有結尾表達式的函數,因為 Rust 是一門基于表達式(expression-based的語言,這是一個需要了解的(不同于其他語言)重要差別。其他語言并沒有這樣的差別,是以讓我們看看語句與表達式有什麼差別以及這些差別是如何影響函數體的。

實際上,我們已經使用過語句和表達式。

語句

是執行一些操作但不傳回值的指令

表達式

計算并産生一個值
fn main(){
	let y = 6 ;
}
           

使y綁定到6是一個語句.

函數定義也是語句,上面整個例子本身就是一個語句。

語句不傳回值。是以,不能把 let 語句指派給另一個變量,比如下面的例子嘗試做的,會産生一個錯誤:

fn main(){
	let x = (let y =6);
}
           

let y = 6 語句并不傳回值,是以沒有可以綁定到 x 上的值

這與其他語言不同,例如 C 和 Ruby,它們的指派語句會傳回所賦的值。在這些語言中,可以這麼寫 x = y = 6,這樣 x 和 y 的值都是 6;Rust 中不能這樣寫。

表達式會計算出一些值,并且你将編寫的大部分 Rust 代碼是由表達式組成的。考慮一個簡單的數學運算,比如 5 + 6,這是一個表達式并計算出值 11。表達式可以是語句的一部分,在語句let y = 6;中6就是一個表達式,其傳回值為6

函數調用是一個表達式。宏調用是一個表達式。我們用來建立新作用域的大括号(代碼塊),{},也是一個表達式,我們看下面的代碼

fn main(){
	let x = 5;  
	let y = {
		let x = 3;
		x+1
	};
	println!("y is {}",y);
}
           

上述代碼中有一個代碼塊{lex x = 3;x+1}其實是一個表達式,他傳回4給y綁定,注意我們這個代碼快最後x+1後面沒有分号;如果我們加了分号他就變成語句,語句不會傳回值,是以y就不會綁定

4.5.2 具有傳回值的函數

函數可以向調用它的代碼傳回值。我們并不對傳回值命名,但要在箭頭(->)後聲明它的類型,如下

fn five->i32{
	5
}
fn main(){
	let x = five;  
	println!{"x is {}",x};
}
           

上面的例子中我們指定了函數5的傳回類型為i32,并不用給傳回值取名字,而函數5後面沒有分号就是一個表達式,具有傳回值,其值就是5,->後指定5的類型為i32,這個函數也是一個表達式,如果我們在5後面加上一個分号那麼這個式子就會報錯,因為他變成了一個語句,語句不會由傳回值,而five函數又定義了傳回值的類型。

4.6 控制流

4.6.1 if表達式

if 表達式允許根據條件執行不同的代碼分支。 如下

fn main(){
	let number = 3;  
	if number < 5{
		println!("contition was true");
	}else{
		println!("condition was false");
	}
}
           

所有的 if 表達式都以 if 關鍵字開頭,其後跟一個條件,if 表達式中與條件關聯的代碼塊有時被叫做 arms,也可以包含一個可選的 else 表達式來提供一個在條件為假時應當執行的代碼塊,如果不提供 else 表達式并且條件為假時,程式會直接忽略 if 代碼塊并繼續執行下面的代碼。

另外值得注意的是代碼中的條件必須是 bool 值,如果條件不是 bool 值,我們将得到一個錯誤,例如

fn main(){
	let number = 3 ; 
	if number {
		println!("number was tree");
	}
}
           

不像 Ruby 或 JavaScript 這樣的語言,Rust 并不會嘗試自動地将非布爾值轉換為布爾值。必須總是顯式地使用布爾值作為 if 的條件

可以将 else if 表達式與 if 和 else 組合來實作多重條件。例如:

fn main(){
	let number = 6; 
	if number % 4 == 0 {
		println!("number is divisible by 4");
	}else if number % 3 == 0 {
		println!("number is divisible by 3");
	}else if number % 2 == 0{
		println!("number is divisible by 2");
	}else{
		println!("numbrt is not divisible by 4,3,2");
	}
}
           

當執行這個程式時,它按順序檢查每個 if 表達式并執行第一個條件為真的代碼塊。注意即使 6 可以被 2 整除,也不會輸出 number is divisible by 2,更不會輸出 else 塊中的 number is not divisible by 4, 3, or 2。原因是 Rust 隻會執行第一個條件為真的代碼塊,并且一旦它找到一個以後,甚至都不會檢查剩下的條件了。

使用過多的 else if 表達式會使代碼顯得雜亂無章,是以如果有多于一個 else if 表達式,最好重構代碼。為此,我們會介紹一個更為強大的 Rust 分支結構(branching construct),叫做 match。

因為 if 是一個表達式,我們可以在 let 語句的右側使用它,我們知道表達式會傳回值,如果在塊中最後一個傳回值不能加分号;看以下的例子

fn main (){
	let condition = true; 
	let number = if condition {
		5
	}else {
		6
	};
	println!("number is {}",number);
}
           

下面還有一個錯誤的代碼

fn main (){
		let condition = true;  
		let number  = if {
			5
		}else{
			”six“
		}
		println!("number is {}",number);
}
           

這個代碼是錯誤的,因為我們的if後的語句在充當表達式要給number指派,而我們的rust聲明變量的時候,變量類型可由綁定的類型去判斷,但是後面的if語句有2個類型的值,一個是int還有一個是string,編譯器需要知道确切的類型好給number複制,是以為了安全考慮會報錯,不允許這樣執行

4.6.2 循環

loop

loop循環我們之前用到了,他就是要叫rust不斷的循環,直到使用者聲明要退出(break,continue),比如如下的代碼就會不斷的運作

fn mian(){
	loop{
		println!("this is loop!!!");
	}
}
           

當運作這個程式時,我們會看到連續的反複列印this is loop!!!,直到我們手動停止程式。大部分終端都支援一個快捷鍵,ctrl-c,來終止一個陷入無限循環的程式

當然我們也可以通過break退出

let loop和傳回的妙用

看以下的代碼

fn main(){
	let mut counter = 0;
	let result = loop{
		counter += 1;
		if counter == 10 {
			break counter *2;
		}
	}; 
	println!("result was {}",result);
}
           

在循環之前,我們聲明了一個名為 counter 的變量并初始化為 0。接着聲明了一個名為 result 來存放循環的傳回值。在循環的每一次疊代中,我們将 counter 變量加 1,接着檢查計數是否等于 10。當相等時,使用 break 關鍵字傳回值 counter * 2。循環之後,我們通過分号結束指派給 result 的語句。最後列印出 result 的值,也就是 20。

while條件循環

while循環的作用大家都知道,注意的是他和if一樣隻接受bool,它可以簡化loop,if else ,break,例子如下

fn main (){
	let mut number = 3;
	while number != 0 {
		println!("{}",number);
		number = number - 1 ;
	}
	println!("LIFTOFF!!!");
}
           
for循環

我們可以用while寫一個循環,周遊數組

fn main (){
		let a = [1,2,3,4,5,6];
		lex mut index = 0;
		
		while index < 6{
			println!("{}",a[0]);
			index = index + 1;
	}
}
           

我們用for循環寫更快

rust的for循環是for i in格式的

fn main(){
	let a =  [10,20,30,40,50];
	for i in a.iter(){
		println!("{}",i);
	}
}
           

如果還是while代碼,我們從代碼中減少一個元素那麼程式就會報錯而for這個并不會,iter是疊代器,後面會提到,他就是周遊容器用的,我們的數組,元組都是容器

我們也可以用for計時,而且非常的友善

fn main (){
	for number in (1..4) {
		println!("{}",number);
	}
	println!("LIFTOFF!!!");
}
           

(1…4)是rust内置的一個資料類型Range,它是标準庫提供的類型,用來生成從一個數字開始到另一個數字之前結束的所有數字的序列。我們還可以在range後面加上rev方法使其反轉,比如(1…4).rev()

4.7 所有權

一些語言中具有垃圾回收機制,在程式運作時不斷地尋找不再使用的記憶體;在另一些語言中,程式員必須親自配置設定和釋放記憶體。Rust 則選擇了第三種方式:通過所有權系統管理記憶體,學習rust的所有權之前先複習一下堆和棧

4.7.1 堆和棧

棧和堆都是代碼在運作時可供使用的記憶體,但是它們的結構不同。棧以放入值的順序存儲值并以相反順序取出值。這也被稱作 後進先出(last in, first out)。想象一下一疊盤子:當增加更多盤子時,把它們放在盤子堆的頂部,當需要盤子時,也從頂部拿走。不能從中間也不能從底部增加或拿走盤子!增加資料叫做 進棧(pushing onto the stack),而移出資料叫做 出棧(popping off the stack)。

棧中的所有資料都必須占用已知且固定的大小。在編譯時大小未知或大小可能變化的資料,要改為存儲在堆上。堆是缺乏組織的:當向堆放入資料時,你要請求一定大小的空間。作業系統在堆的某處找到一塊足夠大的空位,把它标記為已使用,并傳回一個表示該位置位址的 指針(pointer)。這個過程稱作 在堆上配置設定記憶體(allocating on the heap),有時簡稱為 “配置設定”(allocating)。将資料推入棧中并不被認為是配置設定。因為指針的大小是已知并且固定的,你可以将指針存儲在棧上,不過當需要實際資料時,必須通路指針。

通路堆上的資料比通路棧上的資料慢,因為必須通過指針來通路。現代處理器在記憶體中跳轉越少就越快(緩存)。繼續類比,假設有一個服務員在餐廳裡處理多個桌子的點菜。在一個桌子報完所有菜後再移動到下一個桌子是最有效率的。從桌子 A 聽一個菜,接着桌子 B 聽一個菜,然後再桌子 A,然後再桌子 B 這樣的流程會更加緩慢。出于同樣原因,處理器在處理的資料彼此較近的時候(比如在棧上)比較遠的時候(比如可能在堆上)能更好的工作。在堆上配置設定大量的空間也可能消耗時間。

跟蹤哪部分代碼正在使用堆上的哪些資料,最大限度的減少堆上的重複資料的數量,以及清理堆上不再使用的資料確定不會耗盡空間,這些問題正是所有權系統要處理的。一旦了解了所有權,你就不需要經常考慮棧和堆了,不過明白了所有權的存在就是為了管理堆資料,能夠幫助解釋為什麼所有權要以這種方式工作。

4.7.2 rust所有權的規則

  • Rust 中的每一個值都有一個被稱為其 所有者(owner)的變量。
  • 值有且隻有一個所有者。
  • 當所有者(變量)離開作用域,這個值将被丢棄。

作用域

作用域是一個項(item)在程式中有效的範圍。假設有這樣一個變量:

{	 // s 在這裡無效, 它尚未聲明
	let s = "String";   // 從此處起,s 是有效的	
}	// 此作用域已結束,s 不再有效
           
string類型

前面介紹的類型都是存儲在棧上的并且當離開作用域時被移出棧,不過我們需要尋找一個存儲在堆上的資料來探索 Rust 是如何知道該在何時清理資料的。

我們已經見過字元串字面值,字元串值被寫死程序式裡。字元串字面值是很友善的,不過他們并不适合使用文本的每一種場景。原因之一就是他們是不可變的。另一個原因是并不是所有字元串的值都能在編寫代碼時就知道:例如,要是想擷取使用者輸入并存儲該怎麼辦呢?為此,Rust 有第二個字元串類型,String。這個類型被配置設定到堆上,是以能夠存儲在編譯時未知大小的文本

記憶體與配置設定

就字元串字面值來說,我們在編譯時就知道其内容,是以文本被直接寫死進最終的可執行檔案中。這使得字元串字面值快速且高效。不過這些特性都隻得益于字元串字面值的不可變性。不幸的是,我們不能為了每一個在編譯時大小未知的文本而将一塊記憶體放入二進制檔案中,并且它的大小還可能随着程式運作而改變。是以上述的變量s其實還是存儲在棧中的,因為字元串的字面量大小我們已經知曉,而s又是綁定在一個字元串字面值上,不是綁定在字元串上,但是又說string是存儲在堆上的是什麼意思了? 看以下代碼,以下代碼就是存儲在堆上

fn main (){
	let mut s = String::from("hello");	//這個s就是存儲在堆上,因為他綁定在字元串變量上
	s.pust_str(",world");	//在堆中為s追加字元,在棧中無法實作,因為大小都已經在編譯的時候定義死了
	println!("{}",s);
}
           

string::from(“hello”);就是根據這個字元串字面值建立一個字元串變量

首先我們在調用string::from(“hello”);的時候它的實作 (implementation) 請求其所需的記憶體。這在程式設計語言中是非常通用的。

然後回收的機制很多語言都有所不同,垃圾回收(garbage collector,GC)的語言中, GC 記錄并清除不再使用的記憶體,而我們并不需要關心它。沒有 GC 的話,識别出不再使用的記憶體并調用代碼顯式釋放就是我們的責任了,跟請求記憶體的時候一樣。從曆史的角度上說正确處理記憶體回收曾經是一個困難的程式設計問題。如果忘記回收了會浪費記憶體。如果過早回收了,将會出現無效變量。如果重複回收,這也是個 bug。我們需要精确的為一個 allocate 配對一個 free。

rust采用了一個不同的政策,記憶體在擁有他的變量離開作用域後就被自動的釋放

{
    let s = String::from("hello"); // 從此處起,s 是有效的

    // 使用 s
}                                  // 此作用域已結束,
                                   // s 不再有效
           

這個模式對編寫 Rust 代碼的方式有着深遠的影響。現在它看起來很簡單,不過在更複雜的場景下代碼的行為可能是不可預測的,比如當有多個變量使用在堆上配置設定的記憶體時。現在讓我們探索一些這樣的場景。

let x = 5 ; 
let y =x;
           

我們大緻可以猜到這在幹什麼:“将 5綁定到 x;接着生成一個值 x 的拷貝 并綁定到 y”。現在有了兩個變量,x 和 y,都等于 5。這也正是事實上發生了的,因為整數是有已知固定大小的簡單值,是以這兩個 5 被放入了棧中。

string版本

let s1 = String.from("hello");
let s2 = s1;
           

這看起來與上面的代碼非常類似,是以我們可能會假設他們的運作方式也是類似的:也就是說,第二行可能會生成一個 s1 的拷貝并綁定到 s2 上。不過,事實上并不完全是這樣。

String 由三部分組成

  • 一個指向存放字元串内容記憶體的指針
  • 一個長度(String 的内容目前使用了多少位元組的記憶體)
  • 一個容量(String 從作業系統總共擷取了多少位元組的記憶體)

    如下圖

    rust學習筆記1.0 rustup2.0 cargo3.0 一個小項目4.0 rust程式設計基本概念5.0 結構體6.0枚舉和模式比對7.0 包,crate ,項目管理8.0 常見集合9.0 錯誤處理

當我們将 s1 指派給 s2,String 的資料被複制了

這意味着我們從棧上拷貝了它的指針、長度和容量。我們并沒有複制指針指向的堆上資料。換句話說,記憶體中資料的表現如下圖

rust學習筆記1.0 rustup2.0 cargo3.0 一個小項目4.0 rust程式設計基本概念5.0 結構體6.0枚舉和模式比對7.0 包,crate ,項目管理8.0 常見集合9.0 錯誤處理

這樣就有一個問題,如果s1或者s2推出作用域後會清空堆上的空間,如果另一個被指派的變量又離開了作用域,又會第二次清空堆上的空間,這個叫做二次釋放,他是記憶體安全bug之一,兩次釋放(相同)記憶體會導緻記憶體污染,它可能會導緻潛在的安全漏洞。

為了確定記憶體安全,這種場景下 Rust 的處理有另一個細節值得注意。與其嘗試拷貝被配置設定的記憶體,Rust 則認為** s1 不再有效**,是以 Rust 不需要在 s1 離開作用域後清理任何東西。看看在 s2 被建立之後嘗試使用 s1 會發生什麼;這段代碼則會運作錯誤

{
	let s1  =String::from("hello");
	let s2 = s1;  
	println!("{}",s1);
}
           

以上的代碼會運作錯誤,因為S1不生效了

如果你在其他語言中聽說過術語 淺拷貝(shallow copy)和 深拷貝(deep copy),那麼拷貝指針、長度和容量而不拷貝資料可能聽起來像淺拷貝,不過因為 Rust 同時使第一個變量無效了,這個操作被稱為 移動(move),而不是淺拷貝。上面的例子可以解讀為 s1 被 移動 到了 s2 中。那麼具體發生了什麼,如下圖

rust學習筆記1.0 rustup2.0 cargo3.0 一個小項目4.0 rust程式設計基本概念5.0 結構體6.0枚舉和模式比對7.0 包,crate ,項目管理8.0 常見集合9.0 錯誤處理

這樣就解決了我們的問題!因為隻有 s2 是有效的,當其離開作用域,它就釋放自己的記憶體,完畢。

另外,這裡還隐含了一個設計選擇:Rust 永遠也不會自動建立資料的 “深拷貝”。是以,任何自動的複制可以被認為對運作時性能影響較小。如果我們真的想實作深拷貝,也就是移動指針,長度,容量的同時也會移動堆上的資料可以使用string的clone函數,如下圖

let s1 = String:from("hello");
let s2 = s1::clone();
println!("s1 = {},s2 = {}",s1,s2);
           

這樣就會拷貝堆上的資料,當然會資源的消耗會比s2=s1高

這裡還有一個小知識

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

這裡的代碼竟然可以運作,為什麼了?因為x和y存儲在棧上,并沒有存在堆上,資料都是已知的大小,是以拷貝其實際的值是快速的。這意味着沒有理由在建立變量 y 後使 x 無效。換句話說,這裡沒有深淺拷貝的差別,是以這裡調用 clone 并不會與通常的淺拷貝有什麼不同,我們可以不用管它。

Rust 有一個叫做 Copy trait 的特殊注解,可以用在類似整型這樣的存儲在棧上的類型上

那麼什麼類型是可以直接copy而不用clone的勒?一般存儲在棧上的都可以直接copy,所有的簡單标量都可以copy,而複合類型都為普通标量的時候也可以copy,但是其中有一個元素為string就不行,要copy必須要clone

那麼所有權在函數上是什麼樣的勒?

規則非常的簡單,棧中的資料被copy後面還可以繼續用(在此作用域中),而堆中的資料被移動,後面将不會生效

fn main() {
    let s = String::from("hello");  // s 進入作用域

    takes_ownership(s);             // s 的值移動到函數裡 ...
                                    // ... 是以到這裡不再有效

    let x = 5;                      // x 進入作用域

    makes_copy(x);                  // x 應該移動函數裡,
                                    // 但 i32 是 Copy 的,是以在後面可繼續使用 x

} // 這裡, x 先移出了作用域,然後是 s。但因為 s 的值已被移走,
  // 是以不會有特殊操作

fn takes_ownership(some_string: String) { // some_string 進入作用域
    println!("{}", some_string);
} // 這裡,some_string 移出作用域并調用 `drop` 方法。占用的記憶體被釋放

fn makes_copy(some_integer: i32) { // some_integer 進入作用域
    println!("{}", some_integer);
} // 這裡,some_integer 移出作用域。不會有特殊操作
           

如果我們指向獲得其值并不像獲得其使用權勒?我們可以用引用(&),看如下代碼

fn main (){
	let s1 = String::from("hello");
	let len = calculate_length(&s1);  
	println!("the length  of {} is {}",si,len);
}
fn calculate_length (s : &String) -> usize{
	s.len();
}
           

這些 & 符号就是 引用,它們允許你使用值但不擷取其所有權 ,如下圖

rust學習筆記1.0 rustup2.0 cargo3.0 一個小項目4.0 rust程式設計基本概念5.0 結構體6.0枚舉和模式比對7.0 包,crate ,項目管理8.0 常見集合9.0 錯誤處理

看這個函數引用

let len = calculate_length(&s1);
           

&s1 文法讓我們建立一個 指向 值 s1 的引用,但是并不擁有它。因為并不擁有這個值,當引用離開作用域時其指向的值也不會被丢棄。

fn calculate_length(s: &String) -> usize { // s 是對 String 的引用
    s.len()
} // 這裡,s 離開了作用域。但因為它并不擁有引用值的所有權,
  // 是以什麼也不會發生
           

變量 s 有效的作用域與函數參數的作用域一樣,不過當引用離開作用域後并不丢棄它指向的資料,因為我們沒有所有權。當函數使用引用而不是實際值作為參數,無需傳回值來交還所有權,因為就不曾擁有所有權。

我們将擷取引用作為函數參數稱為 借用(borrowing)。正如現實生活中,如果一個人擁有某樣東西,你可以從他那裡借來。當你使用完畢,必須還回去。

如果我們嘗試修改借用的變量呢?這行不通,因為借用一個東西後你要原物傳回,不要損壞,這樣比喻非常的形象 ,看如下代碼,這樣運作會報錯

fn main(){
	let s = String::from("hello");
	change ("s");
}
fn change(some_string:&String){
	some_string.push_char(",world);
}
           

正如變量預設是不可變的,引用也一樣。(預設)不允許修改引用的值。

我們的應用預設可以修改其應用的值,我們還有一個引用,可以修改其引用的值,這個引用叫做可變引用

可變引用就是在引用的類型前面加上一個mut表示可變即可,看如下的代碼

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
           

不過可變引用有一個很大的限制:在特定作用域中的特定資料有且隻有一個可變引用,如下的代碼就會報錯

let mut s = String::from("hello");
let r1 = &mut s;  
let r2 = &mut s;  
println!("{},{}",r1,r2);
           

這個限制的好處是 Rust 可以在編譯時就避免資料競争。資料競争(data race)類似于競态條件,它可由這三個行為造成:

  • 兩個或更多指針同時通路同一資料。
  • 至少有一個指針被用來寫入資料。
  • 沒有同步資料通路的機制。

資料競争會導緻未定義行為,難以在運作時追蹤,并且難以診斷和修複;Rust 避免了這種情況的發生,因為它甚至不會編譯存在資料競争的代碼!以下的代碼就不會報錯

let mut s = String::from("hello");

{
    let r1 = &mut s;

} // r1 在這裡離開了作用域,是以我們完全可以建立一個新的引用

let r2 = &mut s;
           

我們 也 不能在擁有不可變引用的同時擁有可變引用。不可變引用的使用者可不希望在他們的眼皮底下值就被意外的改變了!然而,多個不可變引用是可以的,因為沒有哪個隻能讀取資料的人有能力影響其他人讀取到的資料,例如下面的程式,運作會出錯

let mut s = String::from("hello");

let r1 = &s; // 沒問題,雖然s可變,但是此處不可變
let r2 = &s; // 沒問題,雖然s可變,但是此處不可變
let r3 = &mut s; // 大問題,雖然s可變,但是上面引用都是不可變

println!("{}, {}, and {}", r1, r2, r3);
           

注意一個引用的作用域從聲明的地方開始一直持續到最後一次使用為止,下面的代碼就不會報錯

let mut s = String::from("hello");

let r1 = &s;  // 沒問題
let r2 = &s;  // 沒問題
println("{} and {}",r1,r2);
// 此位置之後 r1 和 r2 不再使用
let r3 = &mut s;  // 沒問題
println("{}",r3);
           

不可變引用 r1 和 r2 的作用域在 println! 最後一次使用之後結束,這也是建立可變引用 r3 的地方。它們的作用域沒有重疊,是以代碼是可以編譯的。

在具有指針的語言中,很容易通過釋放記憶體時保留指向它的指針而錯誤地生成一個 懸垂指針,所謂懸垂指針是其指向的記憶體可能已經被配置設定給其它持有者。相比之下,在 Rust 中編譯器確定引用永遠也不會變成懸垂狀态:當你擁有一些資料的引用,編譯器確定資料不會在其引用之前離開作用域。

我們在此處建立一個垂直引用,編譯器會報錯

fn main(){
	let reference_to_nothing = dangle();
}
fn dangle()->&String{
	let s = String::from("hello");
	&s
}
           

為什麼勒?

fn dangle() -> &String { // dangle 傳回一個字元串的引用

    let s = String::from("hello"); // s 是一個新字元串

    &s // 傳回字元串 s 的引用
} // 這裡 s 離開作用域并被丢棄。其記憶體被釋放。
  // 危險!
           
字元串slice

字元串slice是 String 中一部分值的引用,如下所示

let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
           

可以使用一個由中括号中的 [starting_index…ending_index] 指定的 range 建立一個 slice,其中 starting_index 是 slice 的第一個位置,ending_index 則是 slice 最後一個位置的後一個值。在其内部,slice 的資料結構存儲了 slice 的開始位置和長度,長度對應于 ending_index 減去 starting_index 的值。是以對于 let world = &s[6…11]; 的情況,world 将是一個包含指向 s 第 7 個位元組(從 1 開始)的指針和長度值 5 的 slice。他的存儲結構如圖所示

rust學習筆記1.0 rustup2.0 cargo3.0 一個小項目4.0 rust程式設計基本概念5.0 結構體6.0枚舉和模式比對7.0 包,crate ,項目管理8.0 常見集合9.0 錯誤處理

如果你就是從0開始可以不寫staring_index如下

let s = String::from("hello");
let slice = &s[0..2]; 
let slice = &s[..2];
           

相同的如果slice包含最後一個位元組,那麼我們也可以省略,如果starting_index和ending_index都省略,那麼代表整個字元串

5.0 結構體

rust的結構體和c語言的結構體大部分都差不多,有一些細微的差别,還有使用方法上rust有一些簡潔操作

首先他和我們開始講的元組有一些相似,和元組一樣,結構體的每一部分可以是不同類型。但不同于元組,結構體需要命名各部分資料以便能清楚的表明其值的意義。由于有了這些名字,結構體比元組更靈活:不需要依賴順序來指定或通路執行個體中的值。

rust結構體也是用struct關鍵字,struct定義執行個體如下

struct User{
	username:String,
	email:String,
	sign_in_count:u64,
	active:bool,
}
           

struct的初始化就是這樣變量名字:變量類型(和c語言是反着的),中間都好隔開,注意最後一個成員還有逗号,如果我們使用這個變量中間也是用的:,使用初始化執行個體如下

let user1 = User{
	email:String::from("[email protected]"),
	username:String::from("elvis.zhang"),
	active:true,
	sign_in_count:1,
};
           

使用這個結構也是這樣:左邊是結構成員名,:右邊是資料,我們也可以初始化特定的成員

user1.email = String::from("[email protected]");
           

注意整個執行個體必須是可變的;Rust 并不允許隻将某個字段标記為可變

同其他任何表達式一樣,我們可以在函數體的最後一個表達式中構造一個結構體的新執行個體,來隐式地傳回這個執行個體

以下顯示了一個 build_user 函數,它傳回一個帶有給定的 email 和使用者名的 User 結構體執行個體。active 字段的值為 true,并且 sign_in_count 的值為 1。

fn build_user(email:String,username:String) ->User{
	User{
		email:email,
		username:username,
		active:true,
		sign_in_count:1,
	}
}
           

這個函數的參數和user的成員的值有重合,我們可以用跟為簡潔的方法去寫他

fn build_user(email:String,username:String) ->User{
	User{
		email,
		username,
		active:true,
		sign_in_count:1,
	}
}
           

這裡省略勒User結構的email成員和username成員的值

如果我們還是上述結構User,不過已經初始化勒一個user1,并且已經指派,然後我們再用結構User初始化一個user2,但是我們的user2的一些值想用user1的值可以這樣寫

let user1 = User{
	email:String::("[email protected]"),
	username:elvis.zhang,
	active:true,
	sign_in_count:1,
},
let user2 = User{
	email:String::("[email protected]"),
	username:wenhua.chen,
	..user1
};
//user2的active值和sign_in_count的值和user1相同
           

sruct結構體所有權

在示例中的 User 結構體的定義中,我們使用了自身擁有所有權的 String 類型而不是 &str 字元串 slice 類型。這是一個有意而為之的選擇,因為我們想要這個結構體擁有它所有的資料,為此隻要整個結構體是有效的話其資料也是有效的。

可以使結構體存儲被其他對象擁有的資料的引用,不過這麼做的話需要用上 生命周期(lifetimes),這是一個第十章會讨論的 Rust 功能。生命周期確定結構體引用的資料有效性跟結構體本身保持一緻。如果你嘗試在結構體中存儲一個引用而不指定生命周期将是無效的,比如這樣:

struct User {
    username: &str,
    email: &str,
    sign_in_count: u64,
    active: bool,
}

fn main() {
    let user1 = User {
        email: "[email protected]",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
}
           

此處我們用結構體寫一個計算正方體面積的式子

struct Rectangle{
        length:u32,
        weight:u32,
}
fn main(){
        let rectangle = Rectangle{
                weight:50,
                length:30
        };
        println!("squra is {}",area(&rectangle));
}

fn area(rectangle:&Rectangle)->u32{
        rectangle.length*rectangle.weight
}

           

注意這裡函數 area 現在被定義為接收一個名叫 rectangle 的參數,其類型是一個結構體 Rectangle 執行個體的不可變借用。前面到過,我們希望借用結構體而不是擷取它的所有權,這樣 main 函數就可以保持 rect1 的所有權并繼續使用它,是以這就是為什麼在函數簽名和調用的地方會有 &。

如果我們想列印出rectangle這個結構體的執行個體可以麻,看一下代碼,會報錯

struct Rectangle{
	weight:u32,
	length:u32,
}
fn main(){
	let rectangle = Rectangle{
	weight:30,
	length:50
	};
	println!("rectangle is {}",rectangle);
}
           

以上的代碼會報錯,因為println!宏不能處理struct,不能将其顯示出來,println! 宏能處理很多類型的格式,不過,{} 預設告訴 println! 使用被稱為Display的格式:意在提供給直接終端使用者檢視的輸出。目前為止見過的基本類型都預設實作了 Display,因為它就是向使用者展示 1 或其他任何基本類型的唯一方式。不過對于結構體,println! 應該用來輸出的格式是不明确的,因為這有更多顯示的可能性:是否需要逗号?需要列印出大括号嗎?所有字段都應該顯示嗎?由于這種不确定性,Rust 不會嘗試猜測我們的意圖,是以結構體并沒有提供一個 Display 實作。

其實結構體在這裡被println!宏看作是debug輸出,也就是在println!中的{}裡加上類型:?也就是println("{:?}");,它可以将結構體rectangle全部輸出,但是編譯器并不知道我們的結構體rectangle在debug這個trait裡面,是以我們要在結構體前面加上注釋表示debug輸出變成這樣

#[derive(Debug)]
struct Rectangle{
	weight:u32,
	length:u32,
}
fn main(){
	let rectangle = Rectangle{
	weight:30,
	length:50
	};
	println!("rectangle is {}",rectangle);
}
           

如果我們的println!裡面不用{:?}而是{:#?}的話可以列的顯示結構體所有成員,{:?}是将所有成員在一行列印出來

5.1方法使用

方法 與函數類似:它們使用 fn 關鍵字和名稱聲明,可以擁有參數和傳回值,同時包含在某處調用該方法時會執行的代碼

方法與函數不同的是,因為它們在結構體的上下文中被定義(或者是枚舉或 trait 對象的上下文),并且它們第一個參數總是 self,它代表調用該方法的結構體執行個體(就是為了友善不用重新再寫一遍結構體名字就用self代替)。

看下列代碼

#[derive(debug)]
struct Rectangle {
	weight:u32, 
	length:u32,
}
impl Rectangle{	//定義Rectangle結構的方法
	fn area(&self)->u32{	//具體方法
		self.weight*self.length
	}
}

fn main(){
	let rectangle = Rectangle{
		weight:30,
		length:20
	};
	println!("squar is {}",rectangle.area);
}
           

注意&self代替了rectangle: &Rectangle,因為該方法位于 impl Rectangle 上下文中是以 Rust 知道 self 的類型是 Rectangle。注意仍然需要在 self 前面加上 &,就像 &Rectangle 一樣。方法可以選擇擷取 self 的所有權,或者像我們這裡一樣不可變地借用 self,或者可變地借用 self,就跟其他參數一樣。

這裡選擇 &self 的理由是我們并不想擷取所有權,隻希望能夠讀取結構體中的資料,而不是寫入。如果想要在方法中改變調用方法的執行個體,需要将第一個參數改為 &mut self

使用方法替代函數,除了可使用方法文法和不需要在每個函數簽名中重複 self 的類型之外,其主要好處在于組織性。我們将某個類型執行個體能做的所有事情都一起放入 impl 塊中,而不是讓将來的使用者在我們的庫中到處尋找 Rectangle 的功能。

在 C/C++ 語言中,有兩個不同的運算符來調用方法:. 直接在對象上調用方法,而 -> 在一個對象的指針上調用方法,這時需要先解引用(dereference)指針。換句話說,如果 object 是一個指針,那麼 object->something() 就像 (*object).something() 一樣。

Rust 并沒有一個與 -> 等效的運算符;相反,Rust 有一個叫 自動引用和解引用(automatic referencing and dereferencing)的功能。方法調用是 Rust 中少數幾個擁有這種行為的地方。

他是這樣工作的:當使用 object.something() 調用方法時,Rust 會自動為 object 添加 &、&mut 或 * 以便使 object 與方法簽名比對。也就是說,這些代碼是等價的:

p1.distance(&p2);
(&p1).distance(&p2);
           
第一行看起來簡潔的多。這種自動引用的行為之是以有效,是因為方法有一個明确的接收者———— self 的類型。在給出接收者和方法名的前提下,Rust 可以明确地計算出方法是僅僅讀取(&self),做出修改(&mut self)或者是擷取所有權(self)。事實上,Rust 對方法接收者的隐式借用讓所有權在實踐中更友好。

5.2.1帶有更多參數的方法

我們這裡再寫一個方法,此方法,再接收一個參數(此前一個參數是self,代表具體執行個體化後的結構)

#[derive(debug)]
struct Rectangle {
	weight:u32, 
	length:u32,
}
impl Rectangle{	//定義Rectangle結構的方法
	fn area(&self)->u32{	//具體方法
		self.weight*self.length
	}
	fn can_hold(&self,other:&Rectangle)->bool{
		self.weight > other.length && self.length > other.length
	}
}

fn main(){
	let rec1 = Rectangle{
		weight:30,
		length:20
	};
	let rec2 = Rectangle{
		weight:50,  
		length:90
	};
	println!("rec1 can hold rec2 is {}",rec1.can_hold(&rec2));
	println!("rec2 can hold rec1 is {}",rec2.can_hold(&rec2));
}
           

rect1.can_hold(&rect2) 傳入了 &rect2,它是一個 Rectangle 的執行個體 rect2 的不可變借用。這是可以了解的,因為我們隻需要讀取 rect2(而不是寫入,這意味着我們需要一個不可變借用),而且希望 main 保持 rect2 的所有權,這樣就可以在調用這個方法後繼續使用它。

5.3 關聯函數

ipml塊裡面不光可以定義方法,也可以定義關聯函數(是函數不是方法),關聯函數作用與結構(未初始化的結構,我們之前就用string的關聯函數from建立一個string變量,string::from() ),而我們的方法是作用與具體的結構比如上面Reactangle結構的具體實作rec1,或者rec2,而且關聯函數用::調用,而方法用.調用

關聯函數和方法在定義上的差別很小,方法調用了self關鍵字代表某個已經具體執行個體化後的結構,而關聯函數不用self,比如我們下面的關聯函數輸入一個值使我們的結構長寬都使一樣的

impl Rectangle{
	fn square(size:u32)->Rectangle{
		Rectangle{weight:size,length:size}
	}
}
           

我們就這樣使用let sq = Rectangle::square(3);這樣初始化了一個長寬都為3的正方形。

6.0枚舉和模式比對

枚舉使啥就不需要具體介紹了,我們這裡直接說rust的枚舉 ,我們假設要處理ip位址資料,我們知道ip位址有可能有ipv4或者ipv6是以枚舉隻用列舉2類,v4或者v6,例子如下

enum IpAddrKind{
	v4,
	v6,
}
           

rust是這樣使用枚舉的成員的

let four = IpAddrKind::v4;  
let six = IpAddrKind::v6;
           

注意枚舉的成員位于其辨別符的命名空間中,并使用兩個冒号分開。這麼設計的益處是現在 IpAddrKind::V4 和 IpAddrKind::V6 都是 IpAddrKind 類型的。例如,接着可以定義一個函數來擷取任何 IpAddrKind:

fn route (ip_type:IpAddrKind)
{	
	...
}
           

現在我們可以使用任一成員調用這個函數

route(IpAddrKind::v4);
route(IpAddrKind::v6);
           

我們還可以聯合struct和枚舉去表示我們的ip位址和類型

enum IpAddrKind{
	v4,
	v6,
} //定義ip類型
struct IpAddr{
	kind:IpAddrKind, 	//這裡使用kind去表示類型
	address:string,	//表示具體的ip位址,位址是string
}
let home = IpAddr{
	kind:IpAddrKind::v4,
	address:string::from("127.0.0.1"),
};
let loopback = IpAddr{
	kind:IpAddrKind::v6,
	address::string::from("::1"),
};
           

這裡我們定義了一個有兩個字段的結構體 IpAddr:IpAddrKind(之前定義的枚舉)類型的 kind 字段和 String 類型 address 字段。我們有這個結構體的兩個執行個體。第一個,home,它的 kind 的值是 IpAddrKind::V4 與之相關聯的位址資料是 127.0.0.1。第二個執行個體,loopback,kind 的值是 IpAddrKind 的另一個成員,V6,關聯的位址是 ::1。我們使用了一個結構體來将 kind 和 address 打包在一起,現在枚舉成員就與值相關聯了。

我們可以有更為簡潔的方法去實作,那就是将資料直接附加到我們的枚舉成員中去,如下

enum IpAddrKind{
	v4(String),
	v6(String),
}
           

我們這樣使用它

let home = IpAddrKind::v4(String::from("127.0.0.1"));
let loopback = IpAddrKind::v6(String::from("::1"));
           

這樣就不需要一個額外的結構體了。用枚舉替代結構體還有另一個優勢:每個成員可以處理不同類型和數量的資料。IPv4 版本的 IP 位址總是含有四個值在 0 和 255 之間的數字部分。如果我們想要将 V4 位址存儲為四個 u8 值而 V6 位址仍然表現為一個 String,這就不能使用結構體了。枚舉則可以輕易處理的這個情況:

enum IpAddrKind{
	v4(u8,u8,u8,u8),
	v6(String),
}
let home = IpAddrKind::v4(127,0,0,1);
let home = IpaddrKind::V6("::1");
           

每一個枚舉都可以存儲不同數量類型的值比如我們下面的message枚舉

enum message{
	Quit,
	Move {x:i32,y:i32};
	Write(String),
	ChangeColor(i32,i32,i32),
}
//Move 包含一個匿名結構體。
           

枚舉也可以使用方法和結構一樣,如下

enum message{
	Quit,
	Move {x:i32,y:i32};
	Write(String),
	ChangeColor(i32,i32,i32),
}
impl message{
	fn call(&self){
		...
	}
}
let m = message::Write(String::from("hello")); //執行個體化message,這裡使用的Write成員
m.call; //調用方法
           

6.1 Option枚舉

Option枚舉位于标準庫中

Option 是标準庫定義的另一個枚舉。Option 類型應用廣泛因為它編碼了一個非常普遍的場景,即一個值要麼有值要麼沒值。從類型系統的角度來表達這個概念就意味着編譯器需要檢查是否處理了所有應該處理的情況,這樣就可以避免在其他程式設計語言中非常常見的 bug。

程式設計語言的設計經常要考慮包含哪些功能,但考慮排除哪些功能也很重要。Rust 并沒有很多其他語言中有的空值功能,

空值的問題在于當你嘗試像一個非空值那樣使用一個空值,會出現某種形式的錯誤。因為空和非空的屬性無處不在,非常容易出現這類錯誤。

然而,空值嘗試表達的概念仍然是有意義的:空值是一個因為某種原因目前無效或缺失的值。

Rust 并沒有空值,不過它确實擁有一個可以編碼存在或不存在概念的枚舉。這個枚舉是 Option,而且它定義于标準庫中,如下

enum Option<T>{
	Some(T),
	None,
}
           

Option 枚舉是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其顯式引入作用域,它的成員也是如此,可以不需要 Option:: 字首來直接使用 Some 和 None,即便如此 Option 也仍是正常的枚舉,Some(T) 和 None 仍是 Option 的成員。 文法是一個我們還未講到的 Rust 功能。它是一個泛型類型參數,我們後面會提到,目前,所有你需要知道的就是 < T > 意味着 Option 枚舉的 Some 成員可以包含任意類型的資料,一下是我們使用nothing的例子

let some_number = Some(5);
let some_string = Some("a string");
let absent_number:Option<i32> = None;
           

如果使用 None 而不是 Some,需要告訴 Rust Option 是什麼類型的,因為編譯器隻通過 None 值無法推斷出 Some 成員儲存的值的類型。

我們看以下的代碼

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;
           

以上的代碼會報錯,因為 Rust 不知道該如何将 Option< i8> 與 i8 相加,因為它們的類型不同

當在 Rust 中擁有一個像 i8 這樣類型的值時,編譯器確定它總是有一個有效的值我們可以自信使用而無需做空值檢查

隻有當使用 Option< i8>(或者任何用到的類型)的時候需要擔心可能沒有值,而編譯器會確定我們在使用值之前處理了為空的情況。

換句話說,在對 Option< T> 進行 T 的運算之前必須将其轉換為 T。通常這能幫助我們捕獲到空值最常見的問題之一:假設某值不為空但實際上為空的情況。

那麼當有一個 Option< T> 的值時,如何從 Some 成員中取出 T 的值來使用它呢?Option< T> 枚舉擁有大量用于各種情況的方法:你可以檢視它的文檔。熟悉 Option< T> 的方法将對你的 Rust 之旅非常有用

6.2 match控制流

match它允許我們将一個值與一系列的模式相比較,并根據相比對的模式執行相應代碼,這個模式可以由字面值,變量,通配符和其他的内容構成,我們之前用到過match,我們再寫一個例子

enum Coin{
	Penny,
	Nickel,
	Dime,
	Quarter,
}
fn vaulue_in_cents(coin:Coin)->u8{
	match coin{
		Coin::Penny => 1, 
		Coin::Nickel => 5,
		Coin::Dime => 10, 
		Coin::Quarter => 25,
	}
}
           

首先,我們列出 match 關鍵字後跟一個表達式,在這個例子中是 coin 的值,coin是我們傳進來枚舉的某個成員

這看起來非常像 if 使用的表達式,不過這裡有一個非常大的差別:對于 if,表達式必須傳回一個布爾值而這裡它可以是任何類型的

match後面的一行行是分支,每一個分支有2個部分,一個模式和一些代碼,第一個分支模式的值是Coin::Penny,而之後的 => 運算符将模式和将要運作的代碼分開。這裡的代碼就僅僅是值 1。每一個分支之間使用逗号分隔。當 match 表達式執行時,它将結果值按順序與每一個分支的模式相比較。如果模式比對了這個值,這個模式相關聯的代碼将被執行。如果模式并不比對這個值,将繼續執行下一個分支,非常類似一個硬币分類器。可以擁有任意多的分支

如果想要在分支中運作多行代碼,可以使用大括号

比對分支的另一個有用的功能是可以綁定比對的模式的部分值,比如下面的程式,

程式背景

1999 年到 2008 年間,美帝在 25 美分的硬币的一側為 50 個州的每一個都印刷了不同的設計。其他的硬币都沒有這種區分州的設計,是以隻有這些 25 美分硬币有特殊的價值。可以将這些資訊加入我們的 enum,通過改變 Quarter 成員來包含一個 State

#[derive(Debug)] //因為州是用枚舉表示println!宏裡面的display沒有枚舉類型   
enum USstate{
	alabama,  
	alaska,
	...
}
enum Coin{
	Penny,
	Nickel, 
	Dime,
	Quarter(USstate),
}
fn value_in_cents(coin:Coin)-> u8 {
	match coin{
		coin::Penny => 1,
		coin::Nickel =>  5,
		coin::Dime => 10,  
		coin::Quarter(state) => {
			println!("{}",state);	//這裡傳進來Coin::Quarter(USstate)類型已經綁定到state上
			25
		},
	}
}
           

我們調用這個函數

value_in_cents(Coin::Quarter(USstate::alabama));
           

當我們的上面穿的參數與match裡面最後一個分支比較的時候USstate::alabama這個類型已經傳遞給state了

6.3比對Option< T>

我們可以想之前的那樣比對,如下代碼

fn plus_one(x:Option<i32>) -> Option<i32>{
	match x{
		None => None,
		Some(i) => i+1,
	}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
           

當調用 plus_one(five) 時,plus_one 函數體中的 x 将會是值 Some(5)。接着将其與每個分支比較。

None => None,
           

值 Some(5) 并不比對模式 None,是以繼續進行下一個分支。

Some(i) => Some(i + 1),
           

Some(5) 與 Some(i) 比對嗎?當然比對!它們是相同的成員。i 綁定了 Some 中包含的值,是以 i 的值是 5。接着比對分支的代碼被執行,是以我們将 i 的值加一并傳回一個含有值 6 的新 Some。

plus_one 的第二個調用,這裡 x 是 None。我們進入 match 并與第一個分支相比較。

None => None,
           

比對上了!這裡沒有值來加一,是以程式結束并傳回 => 右側的值 None,因為第一個分支就比對到了,其他的分支将不再比較。

将 match 與枚舉相結合在很多場景中都是有用的。你會在 Rust 代碼中看到很多這樣的模式:match 一個枚舉,綁定其中的值到一個變量,接着根據其值執行代碼。這在一開始有點複雜,不過一旦習慣了,你會希望所有語言都擁有它!這一直是使用者的最愛。

Rust 也提供了一個模式用于不想列舉出所有可能值的場景。例如,u8 可以擁有 0 到 255 的有效的值,如果我們隻關心 1、3、5 和 7 這幾個值,就并不想必須列出 0、2、4、6、8、9 一直到 255 的值。所幸我們不必這麼做:可以使用特殊的模式 _ 替代:

let some_u8_value = 0u8;
match some_u8_value{
	1 => println!("one");
	3 => println!("three");
	5 => println!("five");
	7 => println!("seven");
	_ => println!("other"); //_代表比對所有,這裡也是比對所有但是在最後,也就是說沒有比對到前面的幾種情況就比對這個分支 
}
           

match 還有另一方面需要讨論。考慮一下 plus_one 函數的這個版本,它有一個 bug 并不能編譯:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}
           

Rust 知道我們沒有覆寫所有可能的情況甚至知道哪些模式被忘記了!Rust 中的比對是 窮盡的(exhaustive):必須窮舉到最後的可能性來使代碼有效。特别的在這個 Option 的例子中,Rust 防止我們忘記明确的處理 None 的情況,這使我們免于假設擁有一個實際上為空的值,這造成了之前提到過的價值億萬的錯誤。

6.4 if let控制流

如果我們之前的match隻想比對一項,可以這樣寫

let some_u8_value = Some(0u8);
match some_u8_value(){
	Some(3) => println!("three");
	_ => (),
}
           

這樣我們隻想比對Some(3)的分支,我們還要寫通配符_很麻煩,這時我們用if let來表達更為簡潔

if let Some(3) = some_u8_value{
	println!("three");
}
           

if let 擷取通過等号分隔的一個模式和一個表達式。它的工作方式與 match 相同,這裡的表達式對應 match 而模式則對應第一個分支

使用 if let 意味着編寫更少代碼,更少的縮進和更少的樣闆代碼。然而,這樣會失去 match 強制要求的窮盡性檢查,if let 也可以加else

我們進一步的擴充上面的貨币檢查程式,統計其非25美分的硬币

let mut counter = 0;  //設定計數器
if let Coin::Quarter(state) = coin{	//	如果coin這個枚舉的具體值等于Coin枚舉的Quarter成員就執行後面的步驟,并且将coin的附加值傳入state中
	println!("{}",state);
}else{
	counter += 1; //計數器加一
}
           

是以我們的if let就是檢查這個枚舉的執行個體是否是我們的枚舉中某一個成員,是的話(如果被比較的枚舉執行個體有附加值,就傳入這個比較的枚舉成員附加向量中去)就執行後面的步驟如果不是也可以執行else

總結就是match就是用來比對枚舉執行個體是不是我們枚舉類型的某一個成員,是的話就執行這個分支,不是就往下走,(必須将枚舉所有的成員可能都寫出來,不然編譯器就會報錯),if let就是match的簡化版,可以隻比對某一個成員。

7.0 包,crate ,項目管理

7.1 包和crate

crate 是一個二進制項或者庫。crate root 是一個源檔案,Rust 編譯器以它為起始點,并構成你的 crate 的根子產品

包(package) 是提供一系列功能的一個或者多個crate

個包會包含有一個 Cargo.toml 檔案,闡述如何去建構這些 crate。

包中所包含的内容由幾條規則來确立。一個包中至多隻能包含一個庫 crate(library crate);包中可以包含任意多個二進制 crate(binary crate);包中至少包含一個 crate,無論是庫的還是二進制的。

當我們輸入cargo new NRW_PROJECT後建立一個工程,Cargo 會給我們的包建立一個 Cargo.toml 檔案。檢視 Cargo.toml 的内容,會發現并沒有提到 src/main.rs,因為 Cargo 遵循的一個約定:src/main.rs 就是一個與包同名的二進制 crate 的 crate 根

同樣的,Cargo 知道如果包目錄中包含 src/lib.rs,則包帶有與其同名的庫 crate,且 src/lib.rs 是 crate 根。crate 根檔案将由 Cargo 傳遞給 rustc 來實際建構庫或者二進制項目。

在此,我們有了一個隻包含 src/main.rs 的包,意味着它隻含有一個名為 my-project 的二進制 crate。如果一個包同時含有 src/main.rs 和 src/lib.rs,則它有兩個 crate:一個庫和一個二進制項,且名字都與包相同。通過将檔案放在 src/bin 目錄下,一個包可以享有多個二進制 crate:每個檔案都是一個分離出來的二進制 crate。

一個 crate 會将一個作用域内的相關功能分組到一起,使得該功能可以很友善地在多個項目之間共享。

我們在之前使用的 rand crate 提供了生成随機數的功能。通過将 rand crate 加入到我們項目的作用域中,我們就可以在自己的項目中使用該功能。rand crate 提供的所有功能都可以通過該 crate 的名字:rand 進行通路。

将一個 crate 的功能保持在其自身的作用域中,可以知曉一些特定的功能是在我們的 crate 中定義的還是在 rand crate 中定義的,這可以防止潛在的沖突。例如,rand crate 提供了一個名為 Rng 的特性(trait)。我們還可以在我們自己的 crate 中定義一個名為 Rng 的 struct。因為一個 crate 的功能是在自身的作用域進行命名的,當我們将 rand 作為一個依賴,編譯器不會混淆 Rng 這個名字的指向。在我們的 crate 中,它指向的是我們自己定義的 struct Rng。我們可以通過 rand::Rng 這一方式來通路 rand crate 中的 Rng 特性(trait)。

7.2 定義子產品來控制作用域的私有性

子產品 讓我們可以将一個 crate 中的代碼進行分組,以提高可讀性與重用性

子產品還可以控制項的 私有性,即項是可以被外部代碼使用的(public),還是作為一個内部實作的内容,不能被外部代碼使用(private)。

餐館中會有一些地方被稱之為 前台(front of house),還有另外一些地方被稱之為 背景(back of house)。前台是招待顧客的地方,在這裡,店主可以為顧客安排座位,服務員接受顧客下單和付款,調酒師會制作飲品。背景則是由廚師工作的廚房,洗碗工的工作地點,以及經理做行政工作的地方組成

我們可以将函數放置到嵌套的子產品中,來使我們的 crate 結構與實際的餐廳結構相同。通過執行

cargo new --lib restaurant
           

來建立一個新的名為 restaurant 的庫,代碼如下

mod front_house{
	mod hosting{
		fn add_to_waitlist(){
		...
		}
		fn seat_at_table() {
		..
		}
	}
	mod serving(){
		fn take_order(){
		...
		}
		fn server_order(){
		...
		}
		fn take_payment(){
		...
		}
	}
}
           

将這個代碼放在src/lib.rs中來定義一些子產品和函數

我們定義一個子產品,是以 mod 關鍵字為起始,然後指定子產品的名字(本例中叫做 front_of_house),并且用花括号包圍子產品的主體。在子產品内,我們還可以定義其他的子產品,就像本例中的 hosting 和 serving 子產品。子產品還可以儲存一些定義的其他項,比如結構體、枚舉、常量、特性、或者函數。

通過使用子產品,我們可以将相關的定義分組到一起,并指出他們為什麼相關。程式員可以通過使用這段代碼,更加容易地找到他們想要的定義,因為他們可以基于分組來對代碼進行導航,而不需要閱讀所有的定義。程式員向這段代碼中添加一個新的功能時,他們也會知道代碼應該放置在何處,可以保持程式的組織性。

在前面我們提到了,src/main.rs 和 src/lib.rs 叫做 crate 根。之是以這樣叫它們的原因是,這兩個檔案的内容都是一個從名為 crate 的子產品作為根的 crate 子產品結構,稱為 子產品樹(module tree),下方是子產品樹

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

           

這個樹展示了一些子產品是如何被嵌入到另一個子產品的(例如,hosting 嵌套在 front_of_house 中)。這個樹還展示了一些子產品是互為 兄弟(siblings) 的,這意味着它們定義在同一子產品中(hosting 和 serving 被一起定義在 front_of_house 中)。繼續沿用家庭關系的比喻,如果一個子產品 A 被包含在子產品 B 中,我們将子產品 A 稱為子產品 B 的 子(child),子產品 B 則是子產品 A 的 父(parent)。注意,整個子產品樹都植根于名為 crate 的隐式子產品下。

7.3引用子產品樹的路徑

路徑有兩種形式:

  • 絕對路徑(absolute path)從 crate 根開始,以 crate 名或者字面值 crate 開頭。
  • 相對路徑(relative path)從目前子產品開始,以 self、super 或目前子產品的辨別符開頭。

絕對路徑和相對路徑都後跟一個或多個由雙冒号(::)分割的辨別符。

回看之前的例子,我們如何調用add_to_waitlist函數?

還是同樣的問題,add_to_waitlist 函數的路徑是什麼?

在我們下面的代碼中對我們上面的代碼做了一些精簡,如下

mod front_of_house {
	mod hosting{
		fn add_to_waitlist(){}
	}
}
pub fn eat_at_restaurant(){
	crate::front_of_house::hosting::add_to_waitlist();
	front_of_house::hosting::add_to_waitlist();
}
           

pub 的那個代碼塊中第一句是用絕對路徑去調用add_to_waitlist,第二句是用相對路徑去調用add_to_waitlist

以上的代碼是不能編譯的

我們擁有 hosting 子產品和 add_to_waitlist 函數的的正确路徑,但是 Rust 不讓我們使用,因為它不能通路私有片段。

子產品不僅對于你組織代碼很有用。他們還定義了 Rust 的 私有性邊界(privacy boundary):這條界線不允許外部代碼了解、調用和依賴被封裝的實作細節是以,如果你希望建立一個私有函數或結構體,你可以将其放入子產品。

Rust 中預設所有項(函數、方法、結構體、枚舉、子產品和常量)都是私有的,但是子子產品中的項可以使用他們父子產品中的項,這是因為子子產品封裝并隐藏了他們的實作詳情,但是子子產品可以看到他們定義的上下文。繼續拿餐館作比喻,把私有性規則想象成餐館的背景辦公室:餐館内的事務對餐廳顧客來說是不可知的,但辦公室經理可以洞悉其經營的餐廳并在其中做任何事情。

Rust 選擇以這種方式來實作子產品系統功能,是以預設隐藏内部實作細節。這樣一來,你就知道可以更改内部代碼的哪些部分而不會破壞外部代碼。你還可以通過使用 pub 關鍵字來建立公共項,使子子產品的内部部分暴露給上級子產品。

使用pub關鍵字暴露路徑

我們改進上面的代碼,使用pub關鍵字暴露hosting子產品

mod front_of_house {
	pub mod hosting{
		fn add_to_waitlist(){}
	}
}
pub fn eat_at_restaurant(){
	crate::front_of_house::hosting::add_to_waitlist();
	front_of_house::hosting::add_to_waitlist();
}
           

還是會報錯

因為在 mod hosting 前添加了 pub 關鍵字,使其變成公有的。伴随着這種變化,如果我們可以通路 front_of_house,那我們也可以通路 hosting。但是 hosting 的 内容(contents) 仍然是私有的;這表明使子產品公有并不使其内容也是公有的。子產品上的 pub 關鍵字隻允許其父子產品引用它。

讓我們繼續将 pub 關鍵字放置在 add_to_waitlist 函數的定義之前,使其變成公有。

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}
           

示例為 mod hosting 和 fn add_to_waitlist 添加 pub 關鍵字使他們可以在 eat_at_restaurant 函數中被調用,這樣就可以編譯成功,為什麼?我們先來看發生了什麼

在絕對路徑,我們從 crate,也就是 crate 根開始。然後 crate 根中定義了 front_of_house 子產品。front_of_house 子產品不是公有的,不過因為 eat_at_restaurant 函數與 front_of_house 定義于同一子產品中(即,eat_at_restaurant 和 front_of_house 是兄弟),我們可以從 eat_at_restaurant 中引用 front_of_house。接下來是使用 pub 标記的 hosting 子產品。我們可以通路 hosting 的父子產品,是以可以通路 hosting。最後,add_to_waitlist 函數被标記為 pub ,我們可以通路其父子產品,是以這個函數調用是有效的!

在相對路徑,其邏輯與絕對路徑相同,除了第一步:不同于從 crate 根開始,路徑從 front_of_house 開始。front_of_house 子產品與 eat_at_restaurant 定義于同一子產品,是以從 eat_at_restaurant 中開始定義的該子產品相對路徑是有效的。接下來因為 hosting 和 add_to_waitlist 被标記為 pub,路徑其餘的部分也是有效的,是以函數調用也是有效的!

使用spuer起始的相對路徑

我們還可以使用 super 開頭來建構從父子產品開始的相對路徑。這麼做類似于檔案系統中以 … (上一級目錄)開頭的文法。看我們以下的代碼,如何使用super表示上一級目錄

fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order();
    }

    fn cook_order() {}
}
           

fix_incorrect_order 函數在 back_of_house 子產品中,是以我們可以使用 super 進入 back_of_house 父子產品,也就是本例中的 crate 根。在這裡,我們可以找到 serve_order。成功!我們認為 back_of_house 子產品和 server_order 函數之間可能具有某種關聯關系,并且,如果我們要重新組織這個 crate 的子產品樹,需要一起移動它們。是以,我們使用 super,這樣一來,如果這些代碼被移動到了其他子產品,我們隻需要更新很少的代碼。

建立共有類型和枚舉

我們還可以使用 pub 來設計公有的結構體和枚舉,不過有一些額外的細節需要注意。如果我們在一個結構體定義的前面使用了 pub ,這個結構體會變成公有的,但是這個結構體的字段仍然是私有的

我們可以根據情況決定每個字段是否公有。看下方代碼

mod back_of_house{
	pub struct Breakfast{
		pub toast:String,
		seasonal_fruit:String,
	}
	impl Breakfast{
		pub fn summer(toast:&str)->Breakfast{
			Breakfast{
				toast:String.from(toast),
				seasonal_fruit:String::from("peaches"),
			}
		}
	}
}

pub fn eat_at_restaurant(){
	let mut meal = back_of_house::Breakfast::summer("rye");
	meal.toast = String::from("Wheat");
	println! ("i`d like {} toast please",meal.toast);
}
           

我們定義了一個公有結構體 back_of_hous:Breakfast,其中有一個公有字段 toast 和私有字段 seasonal_fruit

因為 back_of_house::Breakfast 結構體的 toast 字段是公有的,是以我們可以在 eat_at_restaurant 中使用點号來随意的讀寫 toast 字段。注意,我們不能在 eat_at_restaurant 中使用 seasonal_fruit 字段,因為 seasonal_fruit 是私有的

還請注意一點,因為 back_of_house::Breakfast 具有私有字段,是以這個結構體需要提供一個公共的關聯函數來構造示例 Breakfast (這裡我們命名為 summer)。如果 Breakfast 沒有這樣的函數,我們将無法在 eat_at_restaurant 中建立 Breakfast 執行個體,因為我們不能在 eat_at_restaurant 中設定私有字段 seasonal_fruit 的值。

如果我們将枚舉設為公有,則它的所有成員都将變為公有,我們隻需要在 enum 關鍵字前面加上pub

mod back_of_house{
	pub enum Appetizer{
		soup,
		salad;
	}
}
pub fn eat_at_resturant(){
	let order1 = back_to_house::Appetizer::soup;
	let order2 = back_to_house::Appetizer::salad;
}
           

因為我們建立了名為 Appetizer 的公有枚舉,是以我們可以在 eat_at_restaurant 中使用 Soup 和 Salad 成員。如果枚舉成員不是公有的,那麼枚舉會顯得用處不大;給枚舉的所有成員挨個添加 pub 是很令人惱火的,是以枚舉成員預設就是公有的。結構體通常使用時,不必将它們的字段公有化,是以結構體遵循正常,内容全部是私有的,除非使用 pub 關鍵字。

7.4 使用use将名稱引入作用域

mod front_of_house{
	pub mod hosting{
		pub fn add_to_waitlist(){}
	}
}

use crate::front_of_house::hosting;

pub fn eat_at_resturant(){
	hosting::add_to_waitlist();
}
           

我們之前寫的代碼太長了,每一次調用子產品都要寫上絕對路徑或者相對路徑,我們可以使用use關鍵字直接引入這個路徑,就如同他是在本地一樣

我們将 crate::front_of_house::hosting 子產品引入了 eat_at_restaurant 函數的作用域,而我們隻需要指定 hosting::add_to_waitlist 即可在 eat_at_restaurant 中調用 add_to_waitlist 函數。

在作用域中增加 use 和路徑類似于在檔案系統中建立軟連接配接

通過在 crate 根增加 use crate::front_of_house::hosting,現在 hosting 在作用域中就是有效的名稱了,如同 hosting 子產品被定義于 crate 根一樣。通過 use 引入作用域的路徑也會檢查私有性,同其它路徑一樣

你還可以使用 use 和相對路徑來将一個項引入作用域。如下

mod front_to_back{
	pub mod hosting{
		pub fn add_to_waitlist(){
			
		}
	}
}

use front_of_house::hosting;  
pub fn eat_at_restaurant(){
		hosting::add_to_waitlist();
}
           

我們還可以建立慣用的use路徑

你可能會比較疑惑,為什麼我們是指定 use crate::front_of_house::hosting ,然後在 eat_at_restaurant 中調用 hosting::add_to_waitlist ,而不是通過指定一直到 add_to_waitlist 函數的 use 路徑來得到相同的結果

mod front_to_house{
		pub mod hosting{
			pub fn add_waitlist(){}
		}
}
use front_to_house::hosting::add_waitlist; //直接将路徑指到函數
pub fn eat_at_resturant(){
	add_waitlist();
}
           

這樣寫當然可以,但是不符合我們的習慣

以上是使用 use 将函數引入作用域的習慣用法,要想使用 use 将函數的父子產品引入作用域,我們必須在調用函數時指定父子產品,這樣可以清晰地表明函數不是在本地定義的,同時使完整路徑的重複度最小化,以上的代碼不清楚 add_to_waitlist 是在哪裡被定義的。如果不看上面,我們有可能覺得他是在本地定義的,不是use進來的。

相反我們在引入結構體,枚舉等其他的項時,我們一般就use全部(包含結構名字,函數是不包含),因為他是慣例,

use std::collections::HashMap;  
fn main(){
	let mut = HashMap::new();
	map.insert(1,2);
}
           

這個習慣用法有一個例外,那就是我們想使用 use 語句将兩個具有相同名稱的項帶入作用域,因為 Rust 不允許這樣做

use std::fmt;  
use std::io;  
fn function1()->fmt::Result{
	//skip
}
fn function2()->io::Result{
	//skip
}
           

如你所見,使用父子產品可以區分這兩個 Result 類型。如果我們是指定 use std::fmt::Result 和 use std::io::Result,我們将在同一作用域擁有了兩個 Result 類型,當我們使用 Result 時,Rust 則不知道我們要用的是哪個。

如果名字太複雜,我們也可以使用as關鍵字來提供新的名稱 ,如下

use std::fmt::Result as fmtResult;  
use std::io;  
fn function1()->fmtResult{
	//skip
}
fn function2()->Result{
	//skip
}
           

現在我們有這種情況,我們use的時候想要外部的代碼也可以使用,那麼就用pub use關鍵字來引用

mod front_of_house{
	pub mod hosting{
		pub fn add_to_waitlist(){}
	}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant(){
	hosting::add_to_waitlist();
}
           

通過 pub use,現在可以通過新路徑 hosting::add_to_waitlist 來調用 add_to_waitlist 函數。如果沒有指定 pub use,eat_at_restaurant 函數可以在其作用域中調用 hosting::add_to_waitlist,但外部代碼則不允許使用這個新路徑。

使用外部包

在我們之前的猜猜看遊戲中,我們再cargo.toml檔案中的[dependencies]項中加上勒rand項,為了告訴cargo我們要下載下傳這個包,好讓我在程式中使用

[dependencies]
rand = "0.0.5"
           

然後我們在程式的第一行使用use将rand引入到我們的作用域中

,use以包名開始,然後是引入Rng這個trait到作用域中

use rand::Rng;
fn main(){
	let secret_number = rand::thread_rng().gen_range(1,101);
}
           

将 Rng trait 引入作用域并調用了 rand::thread_rng 函數

crates.io 上有很多 Rust 社群成員釋出的包,将其引入你自己的項目都需要一道相同的步驟:在 Cargo.toml 列出它們并通過 use 将其中定義的項引入項目包的作用域中。

注意标準庫(std)對于你的包來說也是外部 crate,因為标準庫随 Rust 語言一同分發,無需修改 Cargo.toml 來引入 std,不過需要通過use将标準庫中定義的項引入項目包的作用域中來引用它們,比如我們使用的 HashMap

use std::collections::HashMap;
           

這是一個以标準庫crate名std開頭的絕對路徑

如果我們引用多個包,子產品,這個包都是屬于一個庫比如std,那麼我們不用都一個一行列出來,可以這樣

use std::cmp::Ordering;  
use std::io;
//....
           

以上是一行一行列出來非常麻煩,可以這樣

use std::{cmp::Ordering,io};
           

我們可以在路徑的任何層級使用嵌套路徑,這在組合兩個共享子路徑的 use 語句時非常有用,如下程式

use std::io;
use std::io::Write;
           

這樣共享了這2個路徑的子路徑,我們可以更友善的調用,然後我們還有更為簡潔的方法去寫這段話

use std::io{self,Write};
           

和上面的式子一樣不過這裡的self是代表std::io自己,這一行便将 std::io 和 std::io::Write 同時引入作用域。

如果我們像将io下的所有項都引入作用域,我們可以用 * 表示如下,這個 * 叫做glob

use std::io::*
           

使用 glob 運算符時請多加小心!Glob 會使得我們難以推導作用域中有什麼名稱和它們是在何處定義的,打比方io下有一個函數,我們使用了以上的glob後可以直接寫這個函數名,但是我們不知道這個函數定義在std::io中還是定義在我們本地。

7.5 将子產品放入不同的檔案内

到目前為止,本章所有的例子都在一個檔案中定義多個子產品,當子產品變得更大時,你可能想要将它們的定義移動到單獨的檔案中,進而使代碼更容易閱讀。

比如我們src/front_of_house.rs檔案裡面的代碼如下

pub mod hosting{
	pub fn add_to_waitlist(){}
}
           

我們像在這個create跟檔案src/lib.rs裡面調用它,或者以 src/main.rs 為 crate 根檔案的二進制 crate 項中調用它如下

mod front_of_house  ;
use crate::front_of_house::hosting;
pub fn eat_at_restaurant(){
	hosting::add_to_waitlist();
}
           

在 mod front_of_house 後使用分号,而不是代碼塊,這将告訴 Rust 在另一個與子產品同名的檔案中加載子產品的内容.

繼續重構我們例子,将 hosting 子產品也提取到其自己的檔案中,僅對 src/front_of_house.rs 包含 hosting 子產品的聲明進行修改:

// src/front_of_house.rs
pub mod hosting;
           

接着我們建立一個 src/front_of_house 目錄和一個包含 hosting 子產品定義的 src/front_of_house/hosting.rs 檔案:

// src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}
           

子產品樹依然保持相同,eat_at_restaurant 中的函數調用也無需修改繼續保持有效

即便其定義存在于不同的檔案中。這個技巧讓你可以在子產品代碼增長時,将它們移動到新檔案中。

8.0 常見集合

Rust 标準庫中包含一系列被稱為 集合(collections)的非常有用的資料結構。大部分其他資料類型都代表一個特定的值,不過集合可以包含多個值。不同于内建的數組和元組類型,這些集合指向的資料是儲存在堆上的,這意味着資料的數量不必在編譯時就已知,并且還可以随着程式的運作增長或縮小。

8.1 vector向量

vector 允許我們在一個單獨的資料結構中儲存多于一個的值,它在記憶體中彼此相鄰地排列所有的值。vector 隻能儲存相同類型的值

我們可以調用Vec::new()來建立一個空的vector,如下

let v:Vec<i32> = Vec::new();
           

以上建立一個空的 vector 來儲存 i32 類型的值

因為沒有向這個 vector 中插入任何值,Rust 并不知道我們想要儲存什麼類型的元素。這是一個非常重要的點。vector 是用泛型實作的,後面會涉及到如何對你自己的類型使用它們。現在,所有你需要知道的就是 Vec 是一個由标準庫提供的類型,它可以存放任何類型,而當 Vec 存放某個特定類型時,那個類型位于尖括号中

在更實際的代碼中,一旦插入值 Rust 就可以推斷出想要存放的類型,是以你很少會需要這些類型注解,如下

let v = vec![1,2,3];
           

以上使用了vec!宏,這個宏會根據我們提供的值來建立一個新的vec,以上建立了一個擁有值1,2,3的vec< i32>

我們可以向vec增加元素,使用put方法

let mut v = Vec::new();
v.push(5);
v.push(6);
...
...
           

如果想要能夠改變它的值,必須使用 mut 關鍵字使其可變。放入其中的所有值都是 i32 類型的,而且 Rust 也根據資料做出如此判斷,是以不需要 Vec< i32> 注解。

vector 在其離開作用域時會被釋放

{
    let v = vec![1, 2, 3, 4];

    // 處理變量 v

} // <- 這裡 v 離開作用域并被丢棄
           

當 vector 被丢棄時,所有其内容也會被丢棄,這意味着這裡它包含的整數将被清理。這可能看起來非常直覺,不過一旦開始使用 vector 元素的引用,情況就變得有些複雜了。下面讓我們處理這種情況!

現在你知道如何建立、更新和銷毀 vector 了,接下來的一步最好了解一下如何讀取它們的内容。

有兩種方法引用 vector 中儲存的值。為了更加清楚的說明這個例子,我們标注這些函數傳回的值的類型,看下面程式

let v = vec![1,2,3,4,5];
let third:&i32 = &v[2];
println!("the third part is {}",third);
match v.get(2){
	Some(third)  => println!("the third part is {}",third);
	None => println!("there is no third part");
}
           

這裡我們用了下标和get方法這2種方式去擷取值,

這裡有兩個需要注意的地方。首先,我們使用索引值 2 來擷取第三個元素,索引是從 0 開始的。其次,這兩個不同的擷取第三個元素的方式分别為:使用 & 和 [] 傳回一個引用

或者使用 get 方法以索引作為參數來傳回一個 Option<&T>。

Rust 有兩個引用元素的方法的原因是程式可以選擇如何處理當索引值在 vector 中沒有對應值的情況。比如我們第二種使用一個Option枚舉

對于通路一個元素下标個數之外的元素,get和下标擷取結果截然不同

let v = vec![1,2,3,4,5];
let three = v[30];
           

這樣會直接報panic!錯誤讓程式執行不下去,如果我們使用get勒

let v = vec![1,2,3,4,5];  
let three = v.get(30);
           

此時three是一個None類型(Option枚舉内),不會報錯,會繼續執行,是以說我們該如何處理越界通路可以采取這2種方式

一旦程式擷取了一個有效的引用,借用檢查器将會執行所有權和借用規則

來確定 vector 内容的這個引用和任何其他引用保持有效。當我們借用一個元素後如果像再在此元素後面加上新值就會報錯

let mut v = vec![1,2,3,4,5];
let first = &v[0];
v.push(6);
println!("{}",first);
           

以上的代碼就會報錯

為什麼第一個元素的引用會關心 vector 結尾的變化?不能這麼做的原因是由于 vector 的工作方式:在 vector 的結尾增加新元素時,在沒有足夠空間将所有所有元素依次相鄰存放的情況下,可能會要求配置設定新記憶體并将老的元素拷貝到新的空間中。這時,第一個元素的引用就指向了被釋放的記憶體。借用規則阻止程式陷入這種狀況。

周遊vector的元素

如果想要依次通路 vector 中的每一個元素,我們可以周遊其所有的元素而無需通過索引一次一個的通路,下圖展示勒如何用for循環來擷取i32值的vector中每一個元素的不可變引用,并将其列印

let v = vec![100,32,57];
for i in &v{
	println!{"{}",i};
}
           

我們也可以周遊可變 vector 的每一個元素的可變引用以便能改變他們。

let mut v = vec!["100,32,57"];
for i in &mut v{
	*i += 50;
}
           

*就是解引用和c++一樣

這裡我們還有一個問題vector的成員必須要求類型一樣,那麼我們如果想讓他不一樣怎麼辦?我們可以使用枚舉,讓vector的每個成員變成枚舉類型,枚舉類型的每個成員可以是不同的變量 ,如下

enum SpreadsheetCell{
	Int(i32),
	Float(f64),
	Text(String),
}
let v = vec![
	SpreadsheetCell::Int(3),
	SpreadsheetCell::Float(1.2),
	SpreadsheetCell::Text(String::from("hello")),		
];
           

Rust 在編譯時就必須準确的知道 vector 中類型的原因在于它需要知道儲存每個元素到底需要多少記憶體。第二個好處是可以準确的知道這個 vector 中允許什麼類型。如果 Rust 允許 vector 存放任意類型,那麼當對 vector 元素執行操作時一個或多個類型的值就有可能會造成錯誤。使用枚舉外加 match 意味着 Rust 能在編譯時就保證總是會處理所有可能的情況

8.2 字元串

我們之前學過字元串,現在我們需要讨論一下字元串具體的意義,rust隻有一種字元串類型str,我們之前說的字元串 slice,它通常以被借用的形式出現, slice它們是一些儲存在别處的 UTF-8 編碼字元串資料的引用。比如字元串字面值被儲存在程式的二進制輸出中,字元串 slice 也是如此。

稱作 String 的類型是由标準庫提供的,而沒有寫進核心語言部分,它是可增長的、可變的、有所有權的、UTF-8 編碼的字元串類型

String 和字元串 slice 都是 UTF-8 編碼的。

Rust 标準庫中還包含一系列其他字元串類型比如 OsString、OsStr、CString 和 CStr。相關庫 crate 甚至會提供更多儲存字元串資料的選擇。看到這些由 String 或是 Str 結尾的名字了嗎?這對應着它們提供的所有權和可借用的字元串變體,就像是你之前看到的 String 和 str。舉例而言,這些字元串類型能夠以不同的編碼,或者記憶體表現形式上以不同的形式,來存儲文本内容。本章将不會讨論其他這些字元串類型,更多有關如何使用它們以及各自适合的場景,請參見其API文檔。

許多vec的操作都可以在String中使用

建立一個空的字元串

let mut s = String::new();
           

這建立了一個叫做 s 的空的字元串,接着我們可以向其中裝載資料。通常字元串會有初始資料,因為我們希望一開始就有這個字元串。為此,可以使用 to_string 方法,它能用于任何實作了 Display trait 的類型,字元串字面值也實作了它。

let data = "init";
let mut s = data.to_string();
//也可以不用變量data這個變量,直接用字元字面量   
let mut s = "init".to_string;
           

也可以使用 String::from 函數來從字元串字面值建立 String。(我們之前常用的)

let s = String::from("init");
           

記住字元串是 UTF-8 編碼的,是以可以包含任何可以正确編碼的資料

let s = String::from("你好");
let s = String::from("😍");
           

所有這些都是有效的 String 值

可以通過 push_str 方法來附加字元串 slice,進而使 String 變長

let mut s = String::from("foo");
s.push_str("bar");
           

使用 push_str 方法向 String 附加字元串 slice

push_str 方法采用字元串 slice,因為我們并不需要擷取參數的所有權,下面的例子表明我們push_str并沒有獲得字元的所有權

let mut s1  = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("{}",s2);
           

可以使用說明沒有擷取s2所有權

如果 push_str 方法擷取了 s2 的所有權,就不能在最後一行列印出其值了

push 方法被定義為擷取一個單獨的字元作為參數,并附加到 String 中

let mut s = String::from("lo");
s.push('l');
           

這樣使用push将字元l添加到lo後面組成lol

我們也可以使用+運算符和format!宏去進行字元拼接

let s1 = String::from("hello,");
let s1 = String::from("friend");
let s3 = s1 + &s2; 
           

注意這裡的s1,已經被移動了,是以s1不能繼續的被使用,s2可以,因為s2沒有移動而是引用

執行完這些代碼之後,字元串 s3 将會包含 Hello, friend

+ 運算符使用了 add 函數,這個函數簽名看起來像這樣

fn add(self,s:&str) -> String{}
           

這并不是标準庫中實際的簽名;标準庫中的 add 使用泛型定義。這裡我們看到的 add 的簽名使用具體類型代替了泛型,這也正是當使用 String 值調用這個方法會發生的,後面會細講

但是我們仔細的看一下add函方法原型,這個方法後面第二個參數是&str,而我們傳入的參數是&String為毛它可以編過?這裡我們回顧一下String和Str,&String和&Str

根本上來講str是常量,我們無法調整str的大小,而String存儲在堆上是變量,可以動态配置設定其大小,String并沒有包含任何str,因為他直接指向一塊記憶體,而str是包含一個固定的str,&str就是單純的指向一個不可變的常量字元,&string也就是一個指針指向String,沒有其所有權

之是以能夠在 add 調用中使用 &s2 是因為 &String 可以被 強轉(coerced)成 &str。當add函數被調用時,Rust 使用了一個被稱為 解引用強制多态(deref coercion)的技術,你可以将其了解為它把 &s2 變成了 &s2[…]

我們上面的方法原型add中第一個參數self沒有用&,那麼說明擷取了s1的所有權,并且s1将不再生效

是以雖然 let s3 = s1 + &s2; 看起來就像它會複制兩個字元串并建立一個新的字元串,而實際上這個語句會擷取 s1 的所有權,附加上從 s2 中拷貝的内容,并傳回結果的所有權。換句話說,它看起來好像生成了很多拷貝,不過實際上并沒有:這個實作比拷貝要更高效。

在比較複雜的字元串拼接上,比如我們有字元串變量,有字元串,而且量非常的大,比如下面

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "_" + &s2 + "_" + &s3;
           

這樣非常複雜難看,我們可以用format!宏

這樣更改

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}_{}_{}",s1,s2,s3);
           

有點像prinln!宏

在很多語言中可以用索引下标去擷取字元串中某個字元是正常操作,但是rust不行比如下面代碼就會報錯

let s = String::from("hello");
let s1  =s[1];
           

說明Rust 的字元串不支援索引。那麼接下來的問題是,為什麼不支援呢?為了回答這個問題,我們必須先聊一聊 Rust 是如何在記憶體中儲存字元串的。

首先String 是一個 Vec< u8> 的封裝

看如下的例子

let len = String::from("hola").len();
           

輸出的是4,因為一個拉丁文占一個位元組,再看下面俄語,我們知道俄語是由西裡爾字母構成,而不是拉丁字母構成,西裡爾字母卻不是一個字母一個位元組,如下

let len = String::from("Здравствуйте").len();
           

rust傳回是24,但是這一共有12個西裡爾字母,說明一個西裡爾字母占2個位元組,說明一個字元串位元組值的索引并不總是對應的一個有效的unicode(一個索引指向一個位元組),是以rust不支援string索引

answer 的值應該是什麼呢?它應該是第一個字元 З 嗎?當使用 UTF-8 編碼時,З 的第一個位元組 208,第二個是 151,是以 answer 實際上應該是 208,不過 208 自身并不是一個有效的字母。傳回 208 可不是一個請求字元串第一個字母的人所希望看到的,不過它是 Rust 在位元組索引 0 位置所能提供的唯一資料。

最後一個 Rust 不允許使用索引擷取 String 字元的原因是,索引操作預期總是需要常數時間 (O(1))。但是對于 String 不可能保證這樣的性能,因為 Rust 必須從開頭到索引位置周遊來确定有多少有效的字元。

索引字元串通常是一個壞點子,因為字元串索引應該傳回的類型是不明确的:位元組值、字元、字形簇或者字元串 slice。是以,如果你真的希望使用索引建立字元串 slice 時,Rust 會要求你更明确一些。為了更明确索引并表明你需要一個字元串 slice,相比使用 [] 和單個值的索引,可以使用 [] 和一個 range 來建立含特定位元組的字元串 slice,比如下方代碼是可以的

let hello = "Здравствуйте";

let s = &hello[0..4];
           

這代表s是一個&str,但是&hello[0…1]就不行

如果你需要操作單獨的 Unicode 标量值(每一個字元),最好的選擇是使用 chars 方法

for i in "नमस्ते".chars(){
	println!("{}",i);
}
           

顯示以下結果

न
म
स
्
त
े  
           

bytes 方法傳回每一個原始位元組

for b in "नमस्ते".bytes() {
    println!("{}", b);
}
           

顯示如下結果

224
164
// --snip--
165
135
           

8.3 hashmap

HashMap<K, V> 類型儲存了一個鍵類型 K 對應一個值類型 V 的映射。它通過一個 哈希函數(hashing function)來實作映射決定如何将鍵和值放入記憶體中。很多程式設計語言支援這種資料結構,不過通常有不同的名字:哈希、map、對象、哈希表或者關聯數組,僅舉幾例。

哈希 map 可以用于需要任何類型作為鍵來尋找資料的情況,而不是像 vector 那樣通過索引。例如,在一個遊戲中,你可以将每個團隊的分數記錄到哈希 map 中,其中鍵是隊伍的名字而值是每個隊伍的分數。給出一個隊名,就能得到他們的得分。

首先我們使用new方法建立一個空的hashmap,并且使用insert插入元素

use std::collections::Hashmap;
let mut scores = Hashmap.new();
scores.insert(String::from("blue"),10);
scores.insert(String::from("yellow"),50);	//元素要一對一對插入
           

首先 use 标準庫中集合部分的 HashMap。在這三個常用集合中,HashMap 是最不常用的,是以并沒有被 prelude 自動引用,是以他也并沒有内建的宏

像 vector 一樣,哈希 map 将它們的資料儲存在堆上,我們上面的例子的鍵類型是string,值類型是i32,和vector一樣,所有的鍵必須是相同類型,值也必須都是相同類型

我們還有别的方法建立hashmap

use std::collections::Hashmap;  
let teams  = vec![String:from("blue"),String:from("yellow")];
let init_scores = vec![10,50];
let scores:Hashmap< _,_> = teams.iter().zip(init_scores.iter()).collect();
           

以上的方法非常巧妙首先建立2個vec分别存儲2個建和2個值,然後最後一行先調用teams的疊代器取出第一個元素,然後再調用zip方法,并且向裡面傳參init_score的疊代器也就是init_score的第一個元素,将這2個元素組成一個元組vector,此vector第一個元素就是元組,元組的成員就是開始2個vector的第一個值,最後調用collect方法将元組vector轉變Hashmap

這裡 HashMap<_, _> 類型注解是必要的,因為可能 collect 很多不同的資料結構,而除非顯式指定否則 Rust 無從得知你需要的類型。

hashmap所有權

hashmap也遵循所有權規則,如果一個值拷貝給别的值後這個值将不生效,如下

use std::collections::Hashmap;
let field_name = String::from("favorite color");
let field_value = String::from("yellow") ;
let mut map = Hashmap.new();
map.insert(field_name,field_value);
//這裡 field_name 和 field_value 不再有效,
           

如果将值的引用插入哈希 map,這些值本身将不會被移動進哈希 map。但是這些引用指向的值必須至少在哈希 map 有效時也是有效的

我們可以通過get方法通過建擷取hashmap的值

use std::collections::Hashmap;
let mut scores = Hashmap::new();
scores.insert(String::from("blue"),10);
scores.insert(String::("yellow"),50);
let team_name = String::from("blue");
let score  = scores.get(&team_name);
           

score 是與藍隊分數相關的值,應為 Some(10)。因為 get 傳回 Option < V>,是以結果被裝進 Some;如果某個鍵在哈希 map 中沒有對應的值,get 會傳回 None

我們也可以使用for周遊hashmap的每一個鍵值對

use std::collections::Hashmap;
let mut scores = Hashmap.new();
scores.insert(String::from("blue"),10);
scores.insert(String::from("yellow"),50);
for (key,value) in &scores{
	println!("{}:{}",key,value);
}
           

這會以任意順序列印出每一個鍵值對:

如果我對一個建連續插入一個值,那麼後插入的就會把前插入的覆寫,比如下面的代碼對blue連續插2此值前一個10會被覆寫

use std::collections::Hashmap;
let mut scores = Hashmap.new();
scores.insert(String::from("blue"),10);
scores.insert(String::from("blue"),20);  
println!("{:?}", scores);
           

我們也可以使用hashmap的entry方法檢查這個建是否存在,或者建對應的值是否存在,如果建不存在,或者建對應的值不存在那麼可以再調用enter方法的or_insert()方法插入,并傳回修改過的 Entry,如果都存在就傳回這個值的 Entry,不插入

以下代碼第一個entry檢查blue建存在,而且值存在,就不修改這個hashmap,并且傳回entry(沒有意義因為沒有綁定),第二個entry檢測到yellow建沒有,然後在hashmap中建立建,并且調用or_insert插入值50.

use std::collections::Hashmap;
let mut scores = Hashmap::new();
scores.insert(String::from("blue"),10);
scores.entry(String::from("blue")).or_insert(50);
scores.entry(String::from("yellow")).or_insert(10);  
println!("{:?}",scores);
           

entry和其or_insert還有很多妙用,我們要先知道就算entry比對裡面的建和值都存在,隻要是entry調用了or_insert都會傳回一個這個建所對應的值的可變引用,我們可以解引用然後改變這個值,意味着引用指向的值(建所對應的值也更改了),看如下代碼

use std::collections::Hashmap;
let txt = "hello world wonderful world";
let mut map = Hashmap::new();
for word in text.split_withspace{	//	text的關聯函數split_withspace将text以空格為界限分開,然我們好進行每一次疊代
	let count = map.entry(word).or_insert(0);	//插入值0,不管word建存不存在,隻要調用了or_insert就會傳回值0的可變引用給count   
	*count += 1; //解引用count并且自身+1,那麼hashmap裡面所對應的值也會改變
}
println!("{:?}",map);
           

9.0 錯誤處理

很多進階語言都有錯誤處理的功能,rust也不例外,rust分可恢複錯誤(recoverable)和 不可恢複錯誤(unrecoverable)

  • 可恢複錯誤(recoverable)

    可恢複錯誤通常代表向使用者報告錯誤和重試操作是合理的情況,比如未找到檔案

  • 不可恢複錯誤(unrecoverable)

    不可恢複錯誤通常是 bug 的同義詞,比如嘗試通路超過數組結尾的位置。

9.1 panic! 與不可恢複的錯誤

我們rust有panic!宏 當執行這個宏時,程式會列印出一個錯誤資訊,展開并清理棧資料,然後接着退出,出現這種情況的場景通常是檢測到一些類型的 bug,而且程式員并不清楚該如何處理它。

當出現 panic 時,程式預設會開始 展開(unwinding),這意味着 Rust 會回溯棧并清理它遇到的每一個函數的資料,不過這個回溯并清理的過程有很多工作。另一種選擇是直接終止(abort),這會不清理資料就退出程式。那麼程式所使用的記憶體需要由作業系統來清理。如果你需要項目的最終二進制檔案越小越好,panic 時通過在 Cargo.toml 的 [profile] 部分增加 panic = ‘abort’,可以由展開切換為終止。例如,如果你想要在release模式中 panic 時直接終止:
[profile.release]
panic = 'abort'
           

假設我們在上述的cargo.homl檔案中定義了panic并且其為abort,那麼我們可以在程式中調用panic宏去使用

fn main (){
	panic!("crash and burn");
}
           

程式會這樣輸出

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.
           

倒數第二行就是我們代碼中panic後的資訊,行和列數,當然也有可能這個panic不在我們的代碼中,而是指向别的代碼,看如下的代碼

fn main (){
	let v = vec![1,2,3];
	v[99];
}
           

這個緩沖區溢出會報panic,Rust 會停止執行并拒絕繼續 ,這個panic報的錯并沒有在我們的代碼中定義,因為這是标準庫中 Vec 的實作。這是當對 vector v 使用 [] 時 vec.rs 中會執行的代碼,也是真正出現 panic! 的地方。這時候我們可以介紹最後一個重要的變量RUST_BACKTRACE

我們可以設定 RUST_BACKTRACE 環境變量來得到一個 backtrace。backtrace 是一個執行到目前位置所有被調用的函數的清單。

閱讀 backtrace 的關鍵是從頭開始讀直到發現你編寫的檔案。這就是問題的發源地,這一行往上是你的代碼所調用的代碼;往下則是調用你的代碼的代碼

讓我們将 RUST_BACKTRACE 環境變量設定為任何不是 0 的值來擷取 backtrace 看看

RUST_BACKTRACE=1 cargo run 
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', libcore/slice/mod.rs:2448:10
stack backtrace:
   0: std::sys::unix::backtrace::tracing::imp::unwind_backtrace
             at libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
   1: std::sys_common::backtrace::print
             at libstd/sys_common/backtrace.rs:71
             at libstd/sys_common/backtrace.rs:59
   2: std::panicking::default_hook::{{closure}}
             at libstd/panicking.rs:211
   3: std::panicking::default_hook
             at libstd/panicking.rs:227
   4: <std::panicking::begin_panic::PanicPayload<A> as core::panic::BoxMeUp>::get
             at libstd/panicking.rs:476
   5: std::panicking::continue_panic_fmt
             at libstd/panicking.rs:390
   6: std::panicking::try::do_call
             at libstd/panicking.rs:325
   7: core::ptr::drop_in_place
             at libcore/panicking.rs:77
   8: core::ptr::drop_in_place
             at libcore/panicking.rs:59
   9: <usize as core::slice::SliceIndex<[T]>>::index
             at libcore/slice/mod.rs:2448
  10: core::slice::<impl core::ops::index::Index<I> for [T]>::index
             at libcore/slice/mod.rs:2316
  11: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
             at liballoc/vec.rs:1653
  12: panic::main
             at src/main.rs:4
  13: std::rt::lang_start::{{closure}}
             at libstd/rt.rs:74
  14: std::panicking::try::do_call
             at libstd/rt.rs:59
             at libstd/panicking.rs:310
  15: macho_symbol_search
             at libpanic_unwind/lib.rs:102
  16: std::alloc::default_alloc_error_hook
             at libstd/panicking.rs:289
             at libstd/panic.rs:392
             at libstd/rt.rs:58
  17: std::rt::lang_start
             at libstd/rt.rs:74
  18: panic::main
           

輸出中,backtrace 的 12 行指向了我們項目中造成問題的行:src/main.rs 的第 4 行,如果你不希望程式 panic,第一個提到我們編寫的代碼行的位置是你應該開始調查的,以便查明是什麼值如何在這個地方引起了 panic

我們故意編寫會 panic 的代碼來示範如何使用 backtrace,修複這個 panic 的方法就是不要嘗試在一個隻包含三個項的 vector 中請求索引是 100 的元素。當将來你的代碼出現了 panic,你需要搞清楚在這特定的場景下代碼中執行了什麼操作和什麼值導緻了 panic,以及應當如何處理才能避免這個問題。

9.2 Result 與可恢複的錯誤

首先我們要明白大部分錯誤并沒有嚴重到需要程式完全停止執行。有時,一個函數會因為一個容易了解并做出反應的原因失敗。例如,如果因為打開一個并不存在的檔案而失敗,此時我們可能想要建立這個檔案,而不是終止程序。

回憶我們之前學到的Result 枚舉,用它來處理潛在錯誤,他的枚舉原型如下

enum Result <T,E>{
	Ok(T),
	Err(E),
}
           

T 和 E 是泛型類型參數後面會講,現在你需要知道的就是 T 代表成功時傳回的 Ok 成員中的資料的類型,而 E 代表失敗時傳回的 Err 成員中的錯誤的類型

讓我們調用一個傳回 Result 的函數,因為它可能會失敗

fn main (){
	let f = File::open("hello.txt");
}
           

如果hello.txt檔案不存在就會報錯,我們如果将f指向一個錯誤的類型也會報錯

fn main(){
	let f:u32 = File::open("hello.txt");
}
           

我們知道File::open函數傳回的是Result<T, E>枚舉而不是u32類型,是以會報錯

這裡T放了成功值的類型 std::fs::File它是一個檔案句柄。E 被用在失敗值上時 E 的類型是 std::io::Error

當 File::open 成功的情況下,變量 f 的值将會是一個包含檔案句柄的 Ok 執行個體。在失敗的情況下,f 的值會是一個包含更多關于出現了何種錯誤資訊的 Err 執行個體。我們如何使用勒?當然是我們的match

use std::fs::File;  

fn main(){
	let f = File::open("hello.txt");
	let f = match f {
		Ok(file) => file,
		Err(error) => println("Problem opening the file{:?}",error);
	};
}
           

注意與 Option 枚舉一樣,Result 枚舉和其成員也被導入到了 prelude 中,是以就不需要在 match 分支中的 Ok 和 Err 之前指定 Result::。

這裡我們告訴 Rust 當結果是 Ok 時,傳回 Ok 成員中的 file 值,然後将這個檔案句柄指派給變量 f。match 之後,我們可以利用這個檔案句柄來進行讀寫。

match 的另一個分支處理從 File::open 得到 Err 值的情況。在這種情況下,我們選擇調用 panic! 宏。如果目前目錄沒有一個叫做 hello.txt 的檔案,當運作這段代碼時會看到如下來自 panic! 宏的輸出:

我們再将上述的代碼進行改進,如果我們的error是找不到檔案error我們就建立一個新的句柄傳回(建立新檔案),如果是因為檔案權限問題那麼我們就panic

use std::fs::File;
 use std::io::ErrorKind;
fn main (){
	f = File::open("hello.txt");
	let f = match f {
		Ok(file) => file,
		Err(error) => match error.kind(){
			ErrorKind::Notfound => macth File::Create("hello.txt"){
				OK(fs) => fs,
				Err(e) => panic(""Problem creating the file: {:?}",e),
			},
			other_error => panic("Problem opening the file: {:?}"other_error),
		},
	};
	
}
           

File::open 傳回的 Err 成員中的值類型 io::Error,它是一個标準庫中提供的結構體。這個結構體有一個傳回 io::ErrorKind 值的 kind 方法可供調用。io::ErrorKind 是一個标準庫提供的枚舉,它的成員對應 io 操作可能導緻的不同錯誤類型。我們感興趣的成員是 ErrorKind::NotFound,它代表嘗試打開的檔案并不存在。是以 match 的 f 比對,不過對于 error.kind() 還有一個内部 match。

我們希望在比對守衛中檢查的條件是 error.kind() 的傳回值是 ErrorKind的 NotFound 成員。如果是,則嘗試通過 File::create 建立檔案。然而因為 File::create 也可能會失敗,還需要增加一個内部 match 語句。當檔案不能被打開,會列印出一個不同的錯誤資訊。外部 match 的最後一個分支保持不變,這樣對任何除了檔案不存在的錯誤會使程式 panic。

match 确實很強大,不過也非常的基礎,後面我們會介紹閉包(closure)。Result<T, E> 有很多接受閉包的方法,并采用 match 表達式實作。一個更老練的 Rustacean 可能會這麼寫:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}
           

match 能夠勝任它的工作,不過它可能有點冗長并且不總是能很好的表明其意圖。Result<T, E> 類型定義了很多輔助方法來處理各種情況

其中之一叫做 unwrap

如果 Result 值是成員 Ok,unwrap 會傳回 Ok 中的值。如果 Result 是成員 Err,unwrap 會為我們調用 panic!,例子如下

use std::fs::File;  
fn main(){
	let f = File::open("hello.txt").unwrap();
}
           

如果hello.txt不在那麼就panic,當然panic内容不是自定義的,如果想自定義可以用expect方法代替unwrap方法,它可以自定義panic

use std::fs::File;
fn main(){
	let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
           

expect 與 unwrap 的使用方式一樣:傳回檔案句柄或調用 panic! 宏。expect 用來調用 panic! 的錯誤資訊将會作為參數傳遞給 expect ,而不像unwrap 那樣使用預設的 panic! 資訊

當編寫一個其實作會調用一些可能會失敗的操作的函數時,除了在這個函數中處理錯誤外,還可以選擇讓調用者知道這個錯誤并決定該如何處理。這被稱為 傳播(propagating)錯誤

以下代碼展示了一個從檔案中讀取使用者名的函數。如果檔案不存在或不能讀取,這個函數會将這些錯誤傳回給調用它的代碼:

use std::io;  
use std::io::Read;
use std::fs::File;  
fn read_username_from_file()->Result< String,io::Error >{
	let f = File::open("hello.txt");
	let mut f = match f {
		Ok(file)=>file,
		Err(e) => return Err(e),
	};
	let mut s = String::new();
	match f.read_to_string(&mut s){
		Ok(_) => Ok(s),
		Err(e) => Err(e)
	}
}
           

首先讓我們看看函數的傳回值:Result<String, io::Error>。這意味着函數傳回一個 Result<T, E> 類型的值,其中泛型參數 T 的具體類型是 String,而 E 的具體類型是 io::Error。如果這個函數沒有出任何錯誤成功傳回,函數的調用者會收到一個包含 String 的 Ok 值

函數從檔案中讀取到的使用者名。如果函數遇到任何錯誤,函數的調用者會收到一個 Err 值,它儲存了一個包含更多這個問題相關資訊的 io::Error 執行個體,這裡選擇 io::Error 作為函數的傳回值是因為它正好是函數體中那兩個可能會失敗的操作的錯誤傳回值:File::open 函數和 read_to_string 方法。

函數體以 File::open 函數開頭。接着使用 match 處理傳回值 Result,類似于上上個示例 中的 match,唯一的差別是當 Err 時不再調用 panic!,而是提早傳回并将 File::open 傳回的錯誤值作為函數的錯誤傳回值傳遞給調用者。如果 File::open 成功了,我們将檔案句柄儲存在變量 f 中并繼續。

變量 s 中建立了一個新 String 并調用檔案句柄 f 的 read_to_string 方法來将檔案的内容讀取到 s 中,read_to_string 方法也傳回一個 Result 因為它也可能會失敗:哪怕是 File::open 已經成功了。是以我們需要另一個 match 來處理這個 Result,如果 read_to_string 成功了,那麼這個函數就成功了,并傳回檔案中的使用者名,它現在位于被封裝進 Ok 的 s 中。如果read_to_string 失敗了,則像之前處理 File::open 的傳回值的 match 那樣傳回錯誤值。不過并不需要顯式的調用 return,因為這是函數的最後一個表達式。

調用這個函數的代碼最終會得到一個包含使用者名的 Ok 值,或者一個包含 io::Error 的 Err 值。我們無從得知調用者會如何處理這些值。例如,如果他們得到了一個 Err 值,他們可能會選擇 panic! 并使程式崩潰、使用一個預設的使用者名或者從檔案之外的地方尋找使用者名。我們沒有足夠的資訊知曉調用者具體會如何嘗試,是以将所有的成功或失敗資訊向上傳播,讓他們選擇合适的處理方法。

這種傳播錯誤的模式在 Rust 是如此的常見,以至于 Rust 提供了 ? 問号運算符來使其更易于處理。

?運算符專門用來傳遞錯誤(不是成功),上述的代碼完全可以寫成下面這樣

use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file()->Result<String,io::Error>{
	let mut f = File::open("hello.txt")?;	//調用?運算符隻傳遞錯誤出去
	let mut s = String::new();
	f.read_to_string(& mut s)?;	//調用?運算符隻傳遞錯誤出去
	Ok(s)	//如果都ok就傳遞ok成功出去其值是s
	
}
           

? 被定義為與上上示例中定義的處理 Result 值的 match 表達式有着完全相同的工作方式。如果 Result 的值是 Ok,這個表達式将會傳回 Ok 中的值而程式将繼續執行。如果值是 Err,Err 中的值将作為整個函數的傳回值,就好像使用了 return 關鍵字一樣,這樣錯誤值就被傳播給了調用者。

match 表達式與問号運算符所做的有一點不同:? 運算符所使用的錯誤值被傳遞給了 from 函數,它定義于标準庫的 From trait 中,其用來将錯誤從一種類型轉換為另一種類型。當 ? 運算符調用 from 函數時,收到的錯誤類型被轉換為定義為目前函數傳回的錯誤類型。這在當一個函數傳回一個錯誤類型來代表所有可能失敗的方式時很有用,即使其可能會因很多種原因失敗。隻要每一個錯誤類型都實作了 from 函數來定義如将其轉換為傳回的錯誤類型,? 運算符會自動處理這些轉換

我們還有更簡單的方法表示

use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file()->Result<String,io::Error>
{
	let mut s = String::new();
	File::open("hello.txt")?.read_to_string(& mut s)?;
	Ok(s)	
}