the rust book 的簡單入門筆記

rust learning

day 1 (2021/05/27)

學了常量,變數,數據類型,控制流,所有權

  1. char 的寬度是4位元組,一個 unicode 的寬度
  2. 控制流條件都不要括弧
  3. rust 中的元組使用和 c++ 中的非常相似
    // clang++ test.cpp -std=c++11 && ./a.out
    #include <iostream>
    #include <string>
    #include <tuple>
    
    int main() {
    std::tuple<int, std::string> t = std::make_tuple(12, "zxh");
    std::cout << std::get<0>(t) << " " << std::get<1>(t) << std::endl;
    
    std::string s;
    int i;
    std::tie(i, s) = std::make_tuple(1, "test-tie");
    std::cout << i << " " << s << std::endl;
    }
    
    // 輸出
    12 zxh
    1 test-tie
    
  4. 所有權
    • 定義非常清晰嚴格,在作用域結束時生命周期結束,賦值操作會發生所有權的轉移,舊值變得不可用(內建類型除外)
    • 同一作用域內,不允許出現兩個可變引用,可以減少產生競態條件的情況
    • 不能在擁有不可變引用的同時擁有可變引用
  5. 切片 slice,在使用和內部實現上和 cpp 的 string_view 或者 golang 的切片都有點類似,對於函數的入參而言使用切片和原始數據類型無差異
  6. 結構體 struct 用法和 cpp 中類似
    • 方法定義等同於 cpp
    • 關聯函數的使用類似於 cpp 中的 static 函數使用
    • #[derive(Debug)] 列印整個結構和值

習題

華氏攝氏度溫度互相轉換

fn c_to_f(c: f32) -> f32 {
    return 1.8 * c + 32.0;
}

fn f_to_c(f: f32) -> f32 {
    return (f - 32.0) / 1.8;
}

列印斐波那契n項值

fn fibonacci(x: u32) -> u32 {
    if x == 0  {
        return 0;
    } else if x == 1 || x == 2 {
        return 1;
    } else {
        return fibonacci(x - 1) + fibonacci(x - 2);
    }
}

TODO

類型系統的轉換看起來還有點不相同,像其它的語言的類型轉換,在 rust 中會報錯

day2 (2021/05/28)

枚舉

在 cpp 中的定義

enum Color { Read, Green };

在 rust 中這樣也可以工作的很好,裡面的所有元素都是相同類型的,在 c 中元素類型默認為 int, cpp 中賦予了更多的類型定義。

更進一步,rust 中允許每個元素 關聯不同的數據類型,如官方教程中的

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

但是既然定義為 enum,那表現的語義就應該是類型相同的,即使內部數據類型有差異,就可以這樣使用了

#[derive(Debug)]
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn foo(m: Message) {
    println!("{:?}", m)
}

fn main() {
    foo(Message::Quit);
    foo(Message::Move { x: 1, y: 2 });
    foo(Message::Write(String::from("test-enum")));
    foo(Message::ChangeColor(1, 2, 3))
}

// output:
//  Quit
//  Move { x: 1, y: 2 }
//  Write("test-enum")
//  ChangeColor(1, 2, 3)

將不同的數據類型聚集在一起來表示同一類型的語義,理論上來說是提高了抽象表達能力的;但看起來又和結構體又十分相似尤其在 cpp 中,一個父類派生出多個子類,然後可以用父類指針來表達到不同的子類上,但在 rust 這種實現用於了枚舉。

Option

實現和 cpp 中的 type_traits 非常類似,在 cpp 中在編譯時通過類型來匹配對應的函數。

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

對於一個函數的返回而言,如果需要返回空則直接返回 None;若是需要返回一個 i32,則返回 Some(i32)。進而函數調用後對結果進行 is_none() 的判斷,如果非空,則可以使用 x.unwrap() 取出值。

再次表達和 cpp 中的 SFINAE 極其相似,看起來還有一些運行時消耗。

match

match 是一個表達式,每個分支相關聯的程式碼是一個表達式,而表達式的結果值將作為整個 match 表達式的返回值。

=> 後每個分支的返回值類型必須相同,對於枚舉的類型系統而言完成了一個閉環。

前面看到枚舉可以由不同類型甚至攜帶不同的參數組成,開始我對枚舉進行判斷相同時的構想是這個樣子的:

fn foo(m: Message) {
    if m == Message::Quit {}
    if m == Message::Write(String::from("compare")) {}
}

當然,上面的程式碼是行不通的,但是從這樣子來看的話,類型判斷還算準確,帶參數的要參數才能匹配,

match 出現後,在上面相同的語義上繼續放大了威力。enum 帶了參數是吧,我match可以對你的參數進行解析,然後中間的過程你自己決定,對於參數而言,不一定要相等,我約等於也可以。

結合 matchenum 才算是真正發揮了設計的效果。

一個語法糖 if let 可以省略 match 表達式,match 中的分支跟在 if let 之後。

day3 (2021/05/30)

rust 包管

rust 的包管理系統都是圍繞著 cargo 來進行的。有幾個相關的概念.

  • 包(Packages): 源文件和描述包的 Cargo.toml 清單文件的集合。
  • Crates :一個模組的樹形結構,是一個庫或一個可執行程式,分別稱為lib crate 或 binary crate。
  • 模組(Modules)和 use: 允許你控制作用域和路徑的私有性。
  • 路徑(path):一個命名例如結構體、函數或模組等項的方式

上面的話是官方的解釋(The Book 和 The Cargo Book).

在 cpp 中,沒有啥包的概念,只有一個命名空間的概念,粗略一看和 rust 中的方式差的遠,golang 和 rust 同屬現代語言,這裡用 golang 做比較會好理解一些。

  • 模組路徑都好理解,理解為文件系統的路徑就行。一般而言模組的名稱就是文件的名稱,golang 中模組名為目錄名。
  • ccrate 和 package 的關係為,package 可以包含至多一個 lib crate 和多個 binary crate。rust 的 crate 功能就和 golang 中的 package main 相似,可以有多個。

root 的概念

  • crate root, src/main|lib.rs 就是一個與包同名的 crate 根。通過將文件放在 src/bin 目錄下,一個包可以擁有多個二進位 crate.
  • package root, 包的 Cargo.toml 文件所在目錄
  • workspace root, 工作區的 Cargo.toml 文件坐在目錄

集合

vector

提供了宏的操作 vec!,作用類似 cpp 中的初始化列表在 vector 中的使用。

由於語法限制了不能多個可變引用,所以就沒有辦法變出這種腦淤血的程式碼,這種程式碼在 cpp 中叫做迭代器失效,rust 直接禁止這樣的寫法

    let mut y = vec!["z", "x", "h"];
    for v in &mut y {
        println!("{}", v);
        y.push("sb");
    }

內部實現和各種 vector 實現無異,都是 data,len,capacity 的實現,增長因子為 2.

與 cpp 比起來,多了一個 pop 操作。

字元串

rust 語言內置字元串類型:str,字元串 slice,它通常以被借用的形式出現,&str. 引用存儲在其它別處的UTF-8的數據,目前這些數據為字面量字元串和String中的數據。

和 cpp 中的 const * char * 有點類似,但是提供了很多隻讀的操作,使用起來比較自然,並且是 unicode 而無需為操蛋的 char wchar 浪費精力。

fn read_str(s: &str) {}

read_str("string-literal");  // OK
let s = "&str"; read_str(s); // OK
let ss = String::from("String"); read_str(&ss); // OK, type coerced

對於需要使用到可變字元串的操作而言,需要使用 String, 同樣是 unicode 編碼,底層實現為 Vec<u8>,不過使用向量不過是方便對數據的存儲,省了一遍造輪子的程式碼。

雖然是向量作為儲存,和 cpp 那種 std::string 表面為字元串,實則為字元數組的的實現不同,String 不支援隨機讀取,

let name = String::from("123舉個🌰");
println!("len: {}", name.len());  // len: 13
println!("{:?}", name.as_bytes());
// [49, 50, 51, 228, 184, 190, 228, 184, 170, 240, 159, 140, 176]
// 1 --> 49
// 2 --> 50
// 3 --> 51
// 舉 --> 228, 184, 190
// 個 --> 190, 228, 170
// 🌰 --> 240, 159, 140, 176

String 不支援隨機存取的操作,但可以使用 slice 來繞過編譯器的檢查,但是一個unicode碼可能是多個位元組組成的,當索引卡在一個字元中間時,直接 panic。

str 作為一個區分,String 有所有權,str 看起來並沒有所有權的一些規則限制,如同i32一般(不過本就是內置類型)

HashMap

map 用著並不自然,存取操作不能使用常規的 map[key] = value. 對於數據的獲取可以說非常反人類了,map[&7] 這種東西都出來了,設計可能合理但是使用不自然。

習題

給定一系列數字,使用 vector 並返回這個列表的平均數(mean, average)、中位數(排列數組後位於中間的值)和眾數(mode,出現次數最多的值)。

// 平均數
fn mean(nums: &Vec<i32>) -> Option<i32> {
    if nums.len() == 0 {
        return None;
    }

    let mut m: i32 = 0;
    for n in nums.iter() {
        m += n;
    }

    return Some(m / nums.len() as i32);
}

// 中位數
fn middle(nums: &Vec<i32>) -> Option<i32> {
    if nums.len() == 0 {
        return None;
    }

    let mut tmp = nums.clone();
    tmp.sort();
    return Some(tmp[tmp.len() / 2]);
}

// 眾數
fn mode(nums: &Vec<i32>) -> Option<i32> {
    if nums.len() == 0 {
        return None;
    }

    let mut freq = HashMap::new();
    for n in nums.iter() {
        let count = freq.entry(n).or_insert(0);
        *count += 1;
    }

    let mut key = nums[0];
    let mut max: i32 = 0;
    for (n, count) in freq.iter() {
        if max < *count {
            max = *count;
            key = **n;
        }
    }

    return Some(key);
}

day4 (2021/05/31)

panic! 中斷程式的運行,使用環境變數 RUST_BACKTRACE=1 可以列印退出程式時的堆棧調用情況。

錯誤錯誤的核心結構為 enum Result.

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

對於一個函數而言,無錯誤發生則返回 Ok(T), 發生錯誤則返回 Err(E).

傳播錯誤的簡寫:? 運算符,工作方式如同以下 match 的邏輯.

let _ = match foo {
    Ok(file) => file,
    Err(e) => return Err(e),
};

foo 為一個 Result,其值為錯誤時,直接作為整個函數的返回值返回。當前來看 ? 運算符只能作用於返回值類型為 Result<T, E> 的函數。

day5 (2021/06/02)

學習泛型

一個普通的 c++ 的泛型相加函數實現如下

template <typename T> T add(T lhs, T rhs) { return lhs + rhs; }
// 特化版本·
template <> int add(int lhs, int rhs) { return lhs * 2 + rhs * 2; }

在 rust 中的實現非常類似,但是這是編譯不過的版本

fn add<T>(x: T, y: T) -> T { x + y }

x + y 的過程是不確認的,如果是兩個自定義類型,可能是不支援相加的操作的,rust 同c++一樣編譯不過去。
和 c++ 不太一樣的是,c++ 是實例化的時候編譯錯誤的,rust 還沒有實例化的時候就提示不對,因為需要提前標註是否支援相加的操作.

形式如下,rust 中需要提前表明這個類型需要支援相加的操作然後在內部才能夠相加

fn add<T: Add<Output = T>>(x: T, y: T) -> T { x + y }

有一點類似 c++ 中的萃取技術,提前預判類型

template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type add2(T lhs, T rhs) {
  return lhs + rhs;
}

在使用上又有點類似於 interface,舉個 golang 的例子,不過 golang 是運行時,如果未實現了 add 方法會 panic(簡單的可以編譯報錯)

type Adder interface {
    add(lhs, rhs int) int
}

type FuckWork struct{}
func (f *FuckWork) add(lhs, rhs int) int { return lhs*2 + rhs*2 }

func fuckAdder(adder interface{}) int {
    ar := adder.(FuckWork)
    return ar.add(1, 2)
}

func main() {
    fw := FuckWork{}
    fmt.Println(fuckAdder(fw)) // 6
}

rust 的 trait 同時吸收了 c++ 的 type_traits 機制和一般語言的 interface 設計。
泛型的設計應該滿足於 trait 才能夠進行,上面的 add 就需要優先表示這個類型是支援 + 的操作。

trait 的定義是一種將方法簽名組合起來的方法,目的是定義一個實現某些目的所必需的行為的集合。這是官方的定義。

個人理解是為了泛型而存在的一種語言特性,在泛型中將相同的操作方法剝離出來,分別對應到不同類型實現上,是一種自底向上的實現;在使用上和 interface 極其類似,唯一的區別就是編譯時。

以下定義了一個評估的 trait,包含了兩個方法,其中一個方法默認實現。

pub trait Evaluate {
    fn height(&self) -> usize;
    fn weight(&self) -> usize { return 0; }
}

實現的方式和寫詩一般 imple xx for XX,為 Persion 這個結構實現了 height 的 trait

struct Person {
    name: String,
}

impl Evaluate for Person {
    fn height(&self) -> usize {
        self.name.len()
    }
}

一個默認和特化實現的 trait 調用方式如下

let p = Person {name:String::from("panda")};
println!("persion {} weight {} and height {}", p.name, p.weight(), p.height());
// output:
// persion panda weight 0 and height 5

接著上面的 rust 泛型 相加的操作,實現了一個自定義的結構的泛型操作

use std::ops::Add;

#[derive(Debug)]
struct FuckWork<T> {
    work_name: T,
    work_type: T,
}

// Notice that the implementation uses the associated type `Output`.
impl<T: Add<Output = T>> Add for FuckWork<T> {
    type Output = Self;

    fn add(self, other: Self) -> Self::Output {
        Self {
            work_name: self.work_name + other.work_name,
            work_type: self.work_type + other.work_type,
        }
    }
}

fn main() {
    let x = FuckWork { work_name: 1, work_type: 2 };
    let y = FuckWork { work_name: 2, work_type: 4 };

    let xf = FuckWork { work_name: 1.0, work_type: 2.0 };
    let yf = FuckWork { work_name: 2.0, work_type: 4.0 };

    println!("{:?}", x + y);    // FuckWork { work_name: 3, work_type: 6 }
    println!("{:?}", xf + yf);  // FuckWork { work_name: 3.0, work_type: 6.0 }
}

但是以上部分並不是對所有的 T 的add 操作都支援的,比如 String,但由於並不允許進行修改,所以在添加一個的 fucker 來說明如何支援更多的相加類型操作。

struct Fucker {
    name: String,
}

impl Add for Fucker {
    type Output = Self;
    fn add(self, other: Self) -> Self::Output {
        Self {
            name: format!("{} say what the fuck? {}\n", self.name, other.name),
        }
    }
}

當有多個類型時,可以這樣對類型實現的 trait 進行限定

fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{
}

生命周期

生命周期沒啥講的,c++裡面已經有非常多的實踐了。

rust 中的生命周期的檢查主要是為了避免垂懸引用的存在,並且在編譯器就需要知道引用的生命周期。

借用檢查器(borrow checker)可以對生命周期進行檢查,當其判斷不了生命周期的長短時,需要使用生命周期註解 ' 來表明生命周期 'a 'b

註解了相同的生命周期則表明變數的生命周期長度是相同的。largest 中的入參和返回的生命周期都是相同的。

fn main() {
    let foo = String::from("first");
    let goo;

    {
        let tmp = String::from("tmp");
        goo = largest(&foo, &tmp);
        println!("{:?}", goo);
    }

    println!("{}", goo);
}

fn largest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() {
        a
    } else {
        b
    }
}

程式碼是編譯不過去的,在函數中表明生命周期是相同的,函數返回後就回到了外層的作用域比較了,很明顯當返回的是 tmp 的引用時,會造成垂懸引用,所以編譯器就報錯了。

error[E0597]: `tmp` does not live long enough
  --> src/main.rs:7:29
   |
7  |         goo = largest(&foo, &tmp);
   |                             ^^^^ borrowed value does not live long enough
8  |         println!("{:?}", goo);
9  |     }
   |     - `tmp` dropped here while still borrowed
10 | 
11 |     println!("{}", goo);
   |                    --- borrow later used here

結構體中的生命周期

含引用的結構體,不過這需要為結構體定義中的每一個引用添加生命周期註解,這是強制性的操作。因為在函數或者其它方法處引用可能與結構體欄位中的引用相關聯。

Fucker 中的兩個引用的生命周期我們註解是相同的,在進入函數是,入參結構體的引用生命周期註解和返回是一樣的,進而說明內部的引用生命周期和返回也是一樣的,編譯器就知道了這個函數的返回生命周期。

struct Fucker<'a> {
    part_a: &'a str,
    part_b: &'a str,
}

fn largest<'a>(f: &'a Fucker) -> &'a str {
    if f.part_a.len() > f.part_b.len() {
        f.part_a
    } else {
        f.part_b
    }
}

生命周期註解省略

函數或方法的參數的生命周期被稱為 輸入生命周期(input lifetimes),而返回值的生命周期被稱為 輸出生命周期(output lifetimes)。

三條可以省略生命周期註解的規則

  1. 每一個是引用的參數都有它自己的生命周期參數。換句話說就是,有一個引用參數的函數有一個生命周期參數:fn foo<'a>(x: &'a i32),有兩個引用參數的函數有兩個不同的生命周期參數,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此類推。
  2. 如果只有一個輸入生命周期參數,那麼它被賦予所有輸出生命周期參數:fn foo<'a>(x: &'a i32) -> &'a i32
  3. 如果方法有多個輸入生命周期參數並且其中一個參數是 &self&mut self,說明是個對象的方法(method), 那麼所有輸出生命周期參數被賦予 self 的生命周期。

三條規則是自上向下(1->3)應用的策略,是一個規則逐個適配的過程。

day6 (2021/06/06)

測試部分,略過,用過 gtest 感覺區別不大

閉包

用法上 rust 和 c++ 都不好用,不夠自然,golang 的用起來最直接舒服。

普通用法和 c++ 一樣,差異在於對於變數捕獲上,c++的值捕獲調用拷貝構造函數,引用捕獲將對象的引用傳遞給匿名函數。
rust 中的所有權機制,導致出現了幾種 trait

  • FnOnce 消費從周圍作用域捕獲的變數,閉包周圍的作用域被稱為其 環境,environment。為了消費捕獲到的變數,閉包必須獲取其所有權並在定義閉包時將其移動進閉包。其名稱的 Once 部分代表了閉包不能多次獲取相同變數的所有權的事實,所以它只能被調用一次。
  • FnMut 獲取可變的借用值所以可以改變其環境
  • Fn 從其環境獲取不可變的借用值

使用起來可以說相當腦殘了,強行和 trait 綁定在一起,感受一下

let mut ms = String::from("test");
let change_str = || ms = String::from("sdf");
change_str(); // 編譯失敗 cannot borrow as mutable

// 修改後的程式碼
let mut ms = String::from("test");
let change_str = || ms = String::from("sdf");
fu_mut(change_str);
fn fu_mut<T>(mut f: T)
where
    T: FnMut(), // 需要說明這個閉包可以修改
{
    f();
}

必要需要實現的這麼複雜的話,不如直接使用一般函數;目前唯一想到的是在寫庫的時候,在庫實現中定義好 FnMut,調用者使用短的閉包就可以了。

迭代器

數據結構實現,唯一的方法就是 next,簡單看了相關結構,和 c++ 中無異。

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

pub struct Iter<'a, T: 'a> {
    ptr: NonNull<T>,
    end: *const T,
    _marker: PhantomData<&'a T>,
}

還是產生其它迭代器的用法簡直花里胡哨的,十足的語法糖,不過也可能增加一些心智負擔。另外一些比較性能的地方可以對比 llvm ir 的實現了。

cargo

cargo 可以根據程式碼中的文檔注釋來生成對應的文檔,格式為 markdown,這個真不錯,對比 doxygen 簡單很多用起來。

day7 (2021/06/07)

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

官方描述對於編譯器而言,Quit 不需要分配記憶體。我猜和 c++ 中的 struct {} 一樣,需要佔用一個位元組的記憶體;對於整個 enum 來說,記憶體佔用的計算和 c++ 中的 union 相同。

細節部分需要通過 llvm ir 得到。

enum List {
    Cons(i32, List),
    Nil,
}

這是一個關於鏈表部分的錯誤定義,Cons 是一個遞歸的寫法,於是編譯器就無法得到具體所佔空間大小了。在 c++ 中同樣編譯不過,需要將 List 變成 *List,rust 中是 Box<T>.

智慧指針

std::boxed::Box 官方的描述是 A pointer type for heap allocation. 結合 rust 的不可變借用規則,和 c++ 中的 std::unique<T> 一毛一樣。

clone 調用是將值複製的一個新的智慧指針對象。

解引用

rust 中的解引用和 c++ 也可以用相同來形容,默認只解 & 的引用,但是 c++ 中是提供重載操作 operator *;rust 中還是 trait,這一點很統一。

MyBox 類型的解引用符需要實現 trait Deref,告知編譯器這個類型可以進行 * 操作,由於是庫實現,本質還是解 &,因為 deref 返回類型還是引用。

struct MyBox<T> {
    value: T,
}

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox { value: x }
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

解引用強制多態

解引用衍生的一個概念,看著像類型推導,但是會調用 deref 還是更像是運行時多態。

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

m 解引用後是 &String 類型,hello 的入參是 &str,但是 String 實現了 Deref trait

impl ops::Deref for String {
    type Target = str;

    #[inline]
    fn deref(&self) -> &str {
        unsafe { str::from_utf8_unchecked(&self.vec) }
    }
}

返回類型為 &str,在進入函數之前,先調用 deref.

強制多態的規則

Rust 在發現類型和 trait 實現滿足三種情況時會進行解引用強制多態:

  • 當 T: Deref<Target=U> 時從 &T 到 &U。
  • 當 T: DerefMut<Target=U> 時從 &mut T 到 &mut U。
  • 當 T: Deref<Target=U> 時從 &mut T 到 &U。

drop trait

rust 中的析構函數也實現為一種 trait,以此支援 RAII,定義如下

pub trait Drop {
    pub fn drop(&mut self);
}

對於希望主動析構 rust 還提構了一個 std::mem::drop(),目前也是看不到有什麼用處,都析構了不如直接放在一個臨時作用域內。

Rc 引用計數智慧指針

翻版 std::shared_ptr<T>,clone 調用並不是真正的調用,而是計數加1

為避免引用循環,又引入了 Weak,熟悉的套路。

std::cell::RefCell

官方的文檔說明是 運行時檢查借用規則的可變記憶體區域.

RefCell<T> 記錄當前有多少個活動的 Ref<T>RefMut<T> 智慧指針。每次調用 borrow,RefCell<T> 將活動的不可變借用計數加一。當 Ref<T> 值離開作用域時,不可變借用計數減一。

核心為將編譯時的規則用在運行時,RefCell<T> 在任何時候只允許有多個不可變借用或一個可變借用

let c = RefCell::new(5);
let b = c.borrow(); // ok
let bb = c.borrow(); // ok
let m = c.borrow_mut(); // panic

結合 Rc<T>RefCell<T> 來擁有多個可變數據所有者,語義和內核的 rcu 驚人的相似,但是語法更為限制。

enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

並發

雖然很多語言覺得增加運行時來換取更多功能沒有什麼問題,但是 Rust 需要做到幾乎沒有運行時,同時為了保持高性能必須能夠調用 C 語言,這點也是不能妥協的

這一點上面,雖然golang可以儘可能少的依賴c相關的東西,但是有些場景根本逃不掉 cgo=1,比如對網路流量進行獲取,一般而言都是基於 libpcap 庫進行開發的,這個時候cgo還是必須打開。

Rust 標準庫只提供了 1:1 執行緒模型實現,這樣的運行時相對較小。

rust 使用 spawn 創建執行緒,join 等待子執行緒的結束。為減少資源的競爭,可以使用 move 關鍵字將所有權轉交至新執行緒中。

use std::thread;

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

    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

rust 中的消息傳遞部分借鑒的 golang,通過隊列 channel 來傳遞消息的。在 rust 中,生產或者消費隊列中的元素都會發生所有權的轉變,使用 Rc<T> 可以存在多所有權,但是看起來管理也是大麻煩。

多生產者單消費者 channel 實現執行緒間的消息同步。

use std::thread;
use std::sync::mpsc;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let vals = vec![1, 2, 3];
        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}

使用互斥變數來同步也是一種方案,使用是將 mutex 和 value 綁定在一起的,不太像 c++ 中的 std::loca_guard<std::mutex> 的用法。

let mutex = Mutex::new(1);
let mut x = mutex.lock().unwrap();
*x = 2;

rust 的面向對象

像 c++ 中的那種繼承派生在 rust 中是不存在的,對於多態而言更像是 golang 中的 interface,rust 的 trait 機制可以實現類似的多態功能。

day8 (2021/06/08)

unsafe 塊用來顯式聲明不安全的程式碼,總體而言還是很自然的,像寫 c 一樣的風格

  1. union、static 這種強規則限制
  2. 繞過語言的一些限制,比如同時同時存在兩個可變引用,這種在 c 裡面是再正常不過的語法
  3. 和 c 交互 ffi
  4. 由上引出的 trait 等一系列複合特性

關聯類型就是別名,類似 c++ 中的泛型實現時會定義一個通用的類型來使用,

typedef _CharT    char_type;

Self 關鍵字為實現 trait 的類型的類型別名,

超trait(supertrait) 為 triat 的依賴聲明,OutlinePrint trait 表明只能用於已經實現過 fmt::Display 的類型才能夠使用。

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        // snip...
    }
}

返回閉包需要用只能指針包裹一層,因為編譯器無法知道具體的所佔空間大小。
返回函數指針或者函數作為入參,都是直接使用函數簽名作為類型。

預覽結束,可以開始寫程式碼了!!!

參考

Rust 程式設計語言,中文版本 the book

Tags: