我與Rust的緣分起始於當我在程式設計論壇上閒逛時,無意間發現了這麼一門現代型的系統安全的函式式系統程式語言,但是當時只是大致瞭解,並無深入學習,所以此次便將它細緻性地學習了一遍。
學習內容為書籍《The Rust Programming language》的全部內容(已完成)、《Rust程式設計之道》的全部內容(未完成)和《The Rustonomicon》的部分內容(未完成)。
一. 內容概述
我將 《Rust 程式語言》 的學習內容分為基礎學習(1至9章)與進階學習(10至19章),這兩個部分是對我學習內容的一個大概縮略。而後是一個根據書上最後一章(20章)進行的簡單的 web server 程式構建,最後是對比 Rust 社群已有的actix web 框架的一個簡單 example。
本文為《The Rust Programming language》前半部分概要,此部分學習練習程式碼已經發在了開源平臺 Gitee 和 GitHub 平臺上.
二. 基礎學習
2.1Rust變數
可變性和不可變性:
Rust變數預設是不可改變的(immutable),而使用mut關關鍵字建立可變變數,例如以下程式:
fn main() {
let x = 5;
println!("The value of x is: {}", x);//5
let mut y = 6;
y = 7
println!("The value of y is: {}", y);//7
}
用如下方法宣告常量:
const MAX_POINTS: u32 = 100_000;
變數遮蔽:使用let關鍵字對變數進行遮蔽,即如下三個x實際上不是同一個變數
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("The value of x is: {}", x);
}
2.2資料型別
Rust是一門面向表示式的函數語言程式設計語言,與我學過的其他兩種函數語言程式設計語言Lisp和Haskell相比,Rust更像是中和了Lisp的抽象和Haskell的型別系統。Rust的每一個語句都是表示式,而每一個表示式都有其返回值,每一個返回值皆有其型別,所以Rust可以說是一切都有型別。且Rust是靜態型別語言,編譯器就必須知道所有變數的型別。
這裡簡單介紹Rust的一些基本原生的資料型別:
2.2.1 標量型別
標量(scalar)型別代表一個單獨的值。Rust 有四種基本的標量型別:整型、浮點型、布林型別和字元型別
整型:
下表展示了Rust原生的整數型別:
長度(bit) | 有符號 | 無符號 |
---|---|---|
8 | i8 | u8 |
16 | i16 | u16 |
32 | i32 | u32 |
64 | i64 | u64 |
128 | i128 | u128 |
arch | isize | usize |
其中arch的有符號整數與無符號整數型別長度(bit)依賴於所執行程式的計算機的架構。
浮點數:
下表展示了Rust原生的浮點數型別:
長度(bit) | 型別 |
---|---|
32 | f32 |
64 | f64 |
Rust原生浮點數採用IEEE-754標準表示,f32為單精度浮點數,f64為雙精度浮點數。
例:
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
數值運算:
Rust 中的所有數字型別都支援基本數學運算:加法、減法、乘法、除法和取餘
布林型別:
Rust布林型別有兩個可能的值:true
和 false
fn main() {
let t = true;
let f: bool = false; // 顯式指定型別註解
}
可以將布林型別轉為整數型別0和1,但是不能將0和1轉為布林型別。
字元型別:
Rust‘的原生字元型別為四個位元組的Unicode標量值,意味著可以表示中文、小表情等字元。
fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '?';
}
2.2.2 複合型別
元組型別:
元組是一個將多個其他型別的值組合進一個複合型別的主要方式。元組長度固定:一旦宣告,其長度不會增大或縮小,使用包含在圓括號中的逗號分隔的值列表來建立一個元組
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;
}
陣列型別:
Rust 中的陣列與一些其他語言中的陣列不同,因為 Rust 中的陣列是固定長度的:一旦宣告,它們的長度不能增長或縮小。陣列是一整塊分配在棧上的記憶體,可以使用索引來訪問陣列的元素。
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
let out = a[5];//陣列越界報錯
}
2.3函式
Rust提供兩種函式,一種是具名函式,一種是匿名函式,匿名函式又被稱為閉包(高階特性).
fn關鍵字被用來指定具名函式,後跟函式名,引數列表和返回值(如果返回值省略則由編譯器自動加上 單元返回值() ),最後則是函式體(實際上是一個塊表示式),Rust 程式碼中的函式和變數名使用 snake case 規範風格。在 snake case 中,所有字母都是小寫並使用下劃線分隔單詞。
fn main() {
let x = plus_one(5);
println!("The value of x is: {}", x);
}
fn plus_one(x: i32) -> i32 {
let y = x;
x + y
}
可以看見函式後面塊表示式的值是最後一個表示式的值,即x+y;而非語句的值(語句返回())
{
let y = x;
x + y
}
2.4註釋
在 Rust 中,註釋必須以兩道斜槓開始,並持續到本行的結尾
// this is a comment
當然,對於程式,Rust有一套標準的文件註釋,比如///和/***/
///this is a doc comment
/**
this is a
multiline
comment
*/
2.5控制流
if表示式:
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!("number is not divisible by 4, 3, or 2");
}
}
用let將if表示式的值獲取:
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
println!("The value of number is: {}", number);
}
loop迴圈表示式:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {}", result);
}
這裡當counter值為10時,跳出loop迴圈並將counter倍乘為20
while條件迴圈表示式:
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number = number - 1;
}
println!("LIFTOFF!!!");
}
條件為真時執行while迴圈,所以這裡while僅迴圈了三次。
for迴圈:
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
}
這裡是返回in後面a呼叫iter()函式生成的迭代器,所以會輸出a裡面的每一個內容。
2.5所有權機制
2.5.1所有權介紹
Rust 的核心功能(之一)是 所有權(ownership),所有權(系統)是 Rust 最為與眾不同的特性,它讓 Rust 無需垃圾回收(garbage collector)即可保障記憶體安全。
實際上,在進行堆區資料管理的時候,一些語言中具有垃圾回收機制(Java、python),在程式執行時不斷地尋找不再使用的記憶體;在另一些語言中,程式設計師必須親自分配和釋放記憶體(c、c++、D)。
Rust的所有權規則為:
- Rust 中的每一個值都有一個被稱為其 所有者(owner)的變數。
- 值在任一時刻有且只有一個所有者。
- 當所有者(變數)離開作用域,這個值將被丟棄。
Rust將所有權轉移的行為稱為move移動,例如以下這段程式碼:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
String::from()函式返回的是一個堆記憶體上變數的指標,如果在c++中,則會形成淺拷貝導致資料競爭或多次釋放形成懸垂指標,造成潛在的安全漏洞,並且如果實現深拷貝會造成效能的降低。而在Rust中則實現了所有權移動,即如下所示:
即變數s1不再有效,不能再使用。
克隆:(深拷貝)
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
此時則會深拷貝堆記憶體變數到另s2,這樣二者都有效,但是這樣造成了效能浪費
2.5.2借用
將獲取引用作為函式引數稱為 借用(borrowing),即不獲取所有權,而獲取操作權:
不可變借用:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
可變借用(可變引用):
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Rust限制了在特定作用域中的特定資料只能有一個可變引用。這個限制的好處是 Rust 可以在編譯時就避免資料競爭.
字串slice:
字串 slice(string slice)是 String 中一部分值的引用,實際上所有的切片型別都是引用.
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
記憶體引用就如同這樣:
2.6結構體
2.6.1基礎結構體
Rust提供三種結構體:
- 具名結構體
- 元組結構體
- 單元結構體
具名結構體及其方法:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {//返回一個結構體例項,功能類似於建構函式
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
impl為實現塊,從上面可以看出一個例項(面嚮物件語言中叫物件)的實現塊可以有多個且可拆分.
元組結構體:
元組結構體有著結構體名稱提供的含義,但沒有具體的欄位名,只有欄位的型別.
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
單元結構體:
struct Empty;
fn main() {
let x = Empty;
println!("{:?}",&x);//單元結構體地址
}
2.6.2 列舉體
列舉體以enum關鍵字定義,後跟列舉體名稱,列舉成員,列舉允許存在不同型別的成員.
enum Message {
Quit,//無參列舉成員
Move { x: i32, y: i32 },//匿名結構體
Write(String),//單參列舉成員
ChangeColor(i32, i32, i32),//參列舉成員
}
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
實際上列舉體是一種特殊的結構體,兩者都能用以建立新型別.
2.7模式匹配
Rust有兩種基礎的控制流運算子可以進行模式匹配.分別是match控制流運算子模式匹配和if let簡潔匹配.
match控制流如例:
let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),//萬用字元匹配剩餘匹配情況
}
if let控制流如例:
enum UsState {
Alabama,
Alaska,
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
let coin = Coin::Penny;
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}
即簡單情況下即用if let進行匹配與失配兩種情況處理.
2.8Rust專案管理
Rust具有完整的模組系統(the module system)用來管理程式碼的組織.
- 包(Packages): Cargo 的一個功能,它允許構建、測試和分享 crate。
- Crates :一個模組的樹形結構,它形成了庫或二進位制專案。
- 模組(Modules)和 use: 允許你控制作用域和路徑的私有性。
- 路徑(path):一個命名例如結構體、函式或模組等項的方式
crate 是一個二進位制項或者庫。crate root 是一個原始檔,Rust 編譯器以它為起始點,並構成crate 的根模組
包(package) 是提供一系列功能的一個或者多個 crate.Crago 是Rust的包管理系統,類似於Java的maven和node.js的npm,一個包會包含有一個 Cargo.toml 檔案,闡述如何去構建這些 crate,以及依賴的外部包.
一個包中至多 只能 包含一個庫 crate(library crate);包中可以包含任意多個二進位制 crate(binary crate);包中至少包含一個 crate,無論是庫的還是二進位制的
模組讓我們可以將一個 crate 中的程式碼進行分組,以提高可讀性與重用性。模組還可以控制項的 私有性,即項是可以被外部程式碼使用的(public),還是作為一個內部實現的內容,不能被外部程式碼使用(private)。
一個包中的眾多模組構成了模組樹,模組不僅對於組織程式碼很有用.還定義了 Rust 的 私有性邊界:這條界線不允許外部程式碼瞭解、呼叫和依賴被封裝的實現細節。
Rust 中預設所有項(函式、方法、結構體、列舉、模組和常量)都是私有的。父模組中的項不能使用子模組中的私有項,但是子模組中的項可以使用他們父模組中的項。這是因為子模組封裝並隱藏了他們的實現詳情,但是子模組可以看到他們定義的上下文。
可以在Rust項之前使用pub
關鍵字使其變為公有.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
可以使用 super
開頭來構建從父模組開始的相對路徑.
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}
fn cook_order() {}
}
可以使用 use
關鍵字呼叫路徑中的項(必須是公開項).use支援巢狀
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
pub mod fleeting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::{hosting,fleeting};
//use crate::front_of_house::*;//全匯入
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
fleeting::add_to_waitlist();
fleeting::add_to_waitlist();
}
可以使用pub use
關鍵字重匯出項.
2.9集合
集合是Rust標準庫中的一系列已經被實現的資料結構.這裡僅記錄三個.
- vector: 允許一個挨著一個地儲存一系列數量可變的值
- String:字元的集合,所以是字串的常用型別.
- map:hash map的Rust標準庫實現,將特定的鍵值對通過雜湊函式關聯.
vector:
vector允許儲存多個相鄰且相同資料型別的值.
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
String:
一種大小可增加,內容可改變的字符集合(字串)
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
String雖然是字串集合,是一個Vec的封裝,但並不能支援索引,因為u8字元的特殊性,操作索引可能會使得u8標量值改變或分離,分解為多個單位元組字元.
map:
HashMap<K, V> 型別儲存了一個鍵型別 K 對應一個值型別 V 的對映。它通過一個 雜湊函式(hashing function)來實現對映,決定如何將鍵和值放入記憶體中.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{:?}", scores);
這段程式碼使用 entry
方法只在鍵沒有對應一個值時插入,所以向scores這個hash map插入了”blue”和50這個鍵值對,輸出{“Yellow”: 50, “Blue”: 10}
2.10錯誤處理
Rust 將錯誤組合成兩個主要類別:可恢復錯誤(recoverable)和 不可恢復錯誤(unrecoverable)。可恢復錯誤通常代表向使用者報告錯誤和重試操作是合理的情況,比如未找到檔案。不可恢復錯誤通常是 bug 的同義詞,比如嘗試訪問超過陣列結尾的位置。
大部分語言並不區分這兩類錯誤,並採用類似異常這樣方式統一處理他們。Rust 並沒有異常,但是,有可恢復錯誤 Result<T, E>
,和不可恢復(遇到錯誤時停止程式執行)錯誤 panic!
panic!:
panic!會導致程式棧的展開(清理棧資料)或終止(不清理棧資料就直接退出程式)
以下是不可恢復錯誤的一個示例:
fn main() {
let v = vec![1, 2, 3];
v[99];
}
主動呼叫panic!:
fn main() {
panic!("crash and burn");
}
console會輸出相應錯誤內容
Result<T, E>:
T
和 E
是泛型型別引數,T
代表成功時返回的 Ok
成員中的資料的型別,而 E
代表失敗時返回的 Err
成員中的錯誤的型別
如下:
use std::fs::File;
fn main() {
let f = File::open("test.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error)
},
};
}
可以使用unwrap進行簡寫,如果 Result
值是成員 Ok
,unwrap
會返回 Ok
中的值。如果 Result
是成員 Err
,unwrap
會為我們呼叫 panic!
use std::fs::File;
fn main() {
let f = File::open("test.txt").unwrap();
}
expect
與 unwrap
的使用方式一樣:返回檔案控制程式碼或呼叫 panic!
巨集。expect
用來呼叫 panic!
的錯誤資訊將會作為引數傳遞給 expect
,而不像unwrap
那樣使用預設的 panic!
資訊.
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
可以用?實現傳播錯誤:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
即?可用於返回Result
基礎部分學習結語:
該部分內容比較簡單,雖然講的都是基礎語法,但是其實細究細節部分也是很有說法的,比如所有權規則、模式匹配和借用檢查器,這些都十分能體現Rust的設計思想。
本作品採用《CC 協議》,轉載必須註明作者和本文連結