《The Rust Programming language》程式碼練習(part 1 基礎部分)

chen0adapter發表於2021-01-17
我與Rust的緣分起始於當我在程式設計論壇上閒逛時,無意間發現了這麼一門現代型的系統安全的函式式系統程式語言,但是當時只是大致瞭解,並無深入學習,所以此次便將它細緻性地學習了一遍。
學習內容為書籍《The Rust Programming language》的全部內容(已完成)、《Rust程式設計之道》的全部內容(未完成)和《The Rustonomicon》的部分內容(未完成)。

一. 內容概述

我將 《Rust 程式語言》 的學習內容分為基礎學習(1至9章)與進階學習(10至19章),這兩個部分是對我學習內容的一個大概縮略。而後是一個根據書上最後一章(20章)進行的簡單的 web server 程式構建,最後是對比 Rust 社群已有的actix web 框架的一個簡單 example。
本文為《The Rust Programming language》前半部分概要,此部分學習練習程式碼已經發在了開源平臺 GiteeGitHub 平臺上.

二. 基礎學習

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布林型別有兩個可能的值:truefalse

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的所有權規則為:

  1. Rust 中的每一個值都有一個被稱為其 所有者owner)的變數。
  2. 值在任一時刻有且只有一個所有者。
  3. 當所有者(變數)離開作用域,這個值將被丟棄。

Rust將所有權轉移的行為稱為move移動,例如以下這段程式碼:

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

println!("{}, world!", s1);

​ String::from()函式返回的是一個堆記憶體上變數的指標,如果在c++中,則會形成淺拷貝導致資料競爭或多次釋放形成懸垂指標,造成潛在的安全漏洞,並且如果實現深拷貝會造成效能的降低。而在Rust中則實現了所有權移動,即如下所示:
ownership
即變數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];

記憶體引用就如同這樣:
字串slice記憶體引用

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>:

TE 是泛型型別引數,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 值是成員 Okunwrap 會返回 Ok 中的值。如果 Result 是成員 Errunwrap 會為我們呼叫 panic!

use std::fs::File;

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

expectunwrap 的使用方式一樣:返回檔案控制程式碼或呼叫 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 協議》,轉載必須註明作者和本文連結

相關文章