一文掌握鋼鐵是怎樣生鏽 -Rust- 的

關注「Rust 編程指北」,一起學習 Rust,給未來投資

五種我認爲值得掌握的現代編程語言:

我刻意剔除了三種大語言 (僅在本文語境下討論,不限實際需求考慮):

你認同麼?我認同,並且我認爲學校教了 C 語言之後,可以直接教 Rust(TODO: 這裏有一些支撐的理由,可以再討論)。

我也直接剔除了各種函數式語言:

函數式語言的的一些範式一直被融入到主流語言裏面,日常開發也幾乎用不到函數式語言,在函數式語言裏面投入時間,邊際收益並不高,但你可以花一個暑假沉浸進去認真感受一次,這樣就夠了。

Rust 開發環境配置

安裝 rustup

在線執行測試 | playground

https://play.rust-lang.org/

安裝 VSCode 插件

掌握 Rust 的命令行工具鏈

Rust 的工程結構

Rust 的模塊組織

img

上圖是 Rust 典型項目文件系統和對應的模塊系統,解釋如下:

Rust 項目根目錄聲明和導出模塊

mod config;
mod manager;
mod objects;
mod util;
mod config;
mod manager;
mod objects; // 含有mod.rs的子目錄是一個子模塊
mod util;    // 含有mod.rs的子目錄是一個子模塊

pub use manager::*; // 指定全導出
pub use objects::AnyObject; // 指定導出objects模塊內的AnyObject

項目子目錄聲明和導出模塊

使用其他模塊

Rust 對象所有權 / 生命週期管理

Linuar Type: https://en.wikipedia.org/wiki/Substructural_type_system

Linear types corresponds to linear logic and ensures that objects are used exactly once, allowing the system to safely deallocate an object after its use.

下面是幾個正交的維度 from : https://www.reddit.com/r/rust/comments/idwlqu/rust_memory_container_cheatsheet_publish_on_github/

Internal sharing? -[no]--> Allocates? -[no]--> Internal mutability? -[no]--> Ownership? -[no]-----------------------------------> &mut T
                                                                                 `-[yes]----------------------------------> T
                                                              \
                                                               `-[yes]-> Thread-safe? -[no]--> Internal references? -[no]---> Cell<T>
                                                                                                                 `-[yes]--> RefCell<T>
                                                                                    \
                                                                                     `-[yes]-> Internal references? -[no]---> AtomicT
                                                                                                                 `-[one]--> Mutex<T>
                                                                                                                  `--[many]-> RwLock<T>
                                  \
                                   `-[yes]------------------------------------------------------------------------------------> Box<T>
                \
                 `-[yes]-> Allocates? -[no]-------------------------------------------------------------------------------------> &T
                                    \
                                     `-[yes]-> Thread-safe? -[no]---------------------------------------------------------------> Rc<T>
                                                           `-[yes]--------------------------------------------------------------> Arc<T>

C++ 生命週期回顧

C++ 從 C 繼承而來,對象生命週期的核心問題是:

先看下對象的生命週期:

再看下對象的狀態管理:

Rust 所有權 Ownership

Rust 引入了一個核心的語義:所有權 (Owner),每個對象都有明確的所有權,所有權可以發生兩種變化,下面是核心規則:

let x=String::from("test"); let y =x;

,賦值語句

let y=x;

x

的所有權移動給

y

,則

x

不再可用

let x=vec![0;32]; let y=& mut x; let z=&mut x; y.push(0);
這裏 y 和 z 都發生了對 x 的可變借用,編譯器會報錯。

- 請在單線程限定下思考這樣設計解決了什麼問題?

Rust 內部可變性 (Internal mutability)

有時候,我們需要【不可變借用的內部成員變量可變,在 Rust 裏面叫做內部可變性 (Internal mutability)】。那麼,有如下選擇,它們內部都依賴底層的UnsafeCell實現,顧名思義這麼做是unsafe的,但是編譯器知道這些調用的地方需要特殊處理。

Cell

對於實現了 Copy 的類型,可以使用 Cell<T>,官方例子:https://doc.rust-lang.org/std/cell/struct.Cell.html

改造下官方例子,官方例子裏只改變了一次不可變借用的 Cell 成員,稍加改造可以多次修改:

use std::cell::Cell;

struct SomeStruct {
    regular_field: u8,
    special_field: Cell<u8>,
}

fn main() {
    let my_struct = SomeStruct {
        regular_field: 0,
        special_field: Cell::new(1),
    };

    // 第1次不可變借用
    let x = &my_struct;

    // 修改1
    x.special_field.set(11);

    println!("{}", x.special_field.get());

    // 第2次不可變借用
    let y = &my_struct;

    // 修改2
    y.special_field.set(3);
    println!("{}", x.special_field.get());

    // 修改3
    x.special_field.set(10);
    println!("{}", x.special_field.get());
}
RefCell

對於沒有實現Copy的類型,例如StringVec<T>,要實現多個不可變借用內部成員的可變性,就需要使用RefCell<T>,常用方法主要是

雖然獲得了對不可變借用內部成員的可變修改能力,但是借用的規則【1】【2】依然起作用,下面是一組單元測試,注意 RefCell 的借用規則在編譯期不會檢查,但是運行期會檢查,如果違反會在運行期 panic。

測試 1:x 一旦 borrow_mut,就不可同時 borrow,借用規則【2】

fn test1(){
    let x = RefCell::new(5);
    let a = x.borrow();
    let b = x.borrow_mut(); // 運行期 panic
}

測試 2:x 的 borrow 可多次,借用規則【1】

fn test2(){
    let x = RefCell::new(5);
    let a = x.borrow();
    let b = x.borrow();
}

測試 3:y 是 x 的 clone,x 和 y 都可多次 borrow,遵循借用規則【1】

fn test3(){
    let x = RefCell::new(5);
    let a = x.borrow();
    let b = x.borrow();

    let y = x.clone();
    let c = y.borrow();
    let d = y.borrow();
}

測試 4:y 是 x 的 clone,x 和 y 一起,只能有一個 borrow_mut,借用規則【2】

fn test4(){
    let x = RefCell::new(5);
    let a = x.borrow_mut();

    let y = x.clone();
    let c = y.borrow_mut();// 運行期 panic
}

測試 5:y 是 x 的 clone,x 和 y 一起,可多次 borrow,借用規則【1】

fn test5(){
    let x = RefCell::new(5);
    let a = x.borrow();

    let y = x.clone();
    let c = y.borrow_mut();
}

測試 6:y 是 x 的 clone,x 和 y 一起,只能有一個 borrow_mut,借用規則【2】,可變借用在超出作用域後歸還,即可再次可變借用

fn test6(){
    let x = RefCell::new(5);
    let y = x.clone();

    {
        let a = x.borrow_mut();
    }

    let c = y.borrow_mut();
}
Mutex/RwLock

無論是 Cell 還是 RefCell,都是單線程語義下達到內部可變性的能力。在多線程情況下,同樣存在一個【不可變借用的內部成員變量可變】的需求。此時,就需要加鎖,Rust 的 Mutext/RwLock 不但實現了鎖的能力,同時提供了內部可變性的能力。

use std::task;
use std::sync::{Mutex, RwLock}

struct Test{
  x: u32
}

// 使用Arc涉及到 內部共享(`Internal sharing`),參考後面
let v = Arc::new(Mutex::new(Test{x:10}))

let v1 = v.clone();
task::spawn(async move {
      // 解鎖+獲得不可變借用
      let v = v1.lock().unwrap();
});

let v2 = v.clone();
task::spawn(async move {
      // 解鎖+獲得可變借用
      let mut v = v1.lock().unwrap();
});

Rust 內存分配 (Allocate)

Rust 的內存分配有三個區域

  1. 程序靜態區 (Static memory),一般是 static 對象

  2. 堆 (Heap), Box,Rc, Arc 以及大部分容器類型 String, Vec, VecDequeue, HashMap, BTreeMap 等,不能在編譯期確定大小

  3. 堆棧 (Stack),除了 #1,#2 外的其他所有 Value 對象都在程序堆棧(Stack) 上分配

Rust 跨線程傳遞 / 共享

對象在跨線程間使用

Sync Trait

根據上面的規則【1】,實際上一個對象從線程 A 傳遞給線程 B 有如下情況

Rust 內部共享 (Internal sharing)

Rust 的有所有權唯一原則,但是有些時候,我們需要在多處持有一個不可變對象的所有權,這叫做內部共享 (Internal sharing) 有兩種情況

組合使用

單線程:

多線程:

Rust 的生命週期 (lifetime)

上面幾個小節都是 Rust 的所有權問題,本節討論 Rust 裏獨立的借用對象的生命週期標識符。

一、函數參數上的 lifetime 標記:(1)首先,Rust 的編譯器需要明確地知道一個借用對象是否還是有效的。例如返回一個新創建的對象肯定是有效的,不需要檢查。

fn create_obj():Object{
  Object{}
}

(2)但是,顯然你不能返回一個局部對象的借用,因爲局部對象在函數結束後超出作用域就被釋放了:

fn get_obj():&Object{ // compile error
  const obj = Object{};
  &obj
}

(3)不過,如果這個借用本來就是從外部傳入的,那當然可以返回,函數結束後這個對象還是有效的:

// I am borrowed from caller
// return borrow to the caller is safe
fn process_obj(obj:&Object):&Object{
  &obj
}

(4)然而,如果你傳入了兩個對象的借用,內部做了條件返回。那麼編譯器沒那麼智能,它並不總是能推斷出返回的是哪個對象的借用:

// compile error!
// where am I come from?
fn process_objs(x:&Obejct, y:&Object):&Object{
  if(x.is_ok()){
    &x
  }else{
    &y
  }
}

(5)因此,Rust 保留了內部的一種編譯器內部的,本來是隱式添加記號,也就是生命週期 (lifetime),通過顯式添加生命週期標記,解決上述問題:

// I am come from 'a lifetime, NOT 'b
fn process_objs<'a,'b>(x: &'a Obejct, y:&'b Object):&'a Object{
  &x
}

// I am come from 'a lifetime, x,y,and result are all 'a lifetime
fn process_objs<'a>(x: &'a Obejct, y:&'a Object):&'a Object{
  if(x.is_ok()){
    &x
  }else{
    &y
  }
}

(6)事實上,當你沒寫 lifetime 標記時,每個對象也都是有對應的 lifetime 的,例如編譯器爲每個對象生成一個不同的 lifetime

fn test<'a,'b>(x: &'a Obejct, y:&'b Object){

}

(7)因爲默認生成的都是不同的,所以返回值如果不標記是誰,編譯器就無法推斷:

fn test<'a,'b,'c>(x: &'a Obejct, y:&'b Object):&'c Object{ // 'c is 'a or 'b ?
  if(x.is_ok()){
    &x
  }else{
    &y
  }
}

(8)所以如果我們顯式標記,並讓兩個變量用同一個,就能解決,這就是告訴編譯器,'c='a='b

fn test<'a>(x: &'a Obejct, y:&'a Object):&'a Object{ // 'c='a='b, they are all 'a
  if(x.is_ok()){
    &x
  }else{
    &y
  }
}

(9)看到這裏,你也應該知道了 lifetime 標記的名字是任意的,只是一個【形參】,代表的是這個借用對象的生命週期作用域的名字:

{
    let obj;                  //---'a start here
    {
        let x = Obj{};        //---'b start here

        obj = &x;             //---'b finish here
    }

    println!("obj: {}", obj); //---'a finish here, 'b is out of scope, compile error!
}

// #[derive(Apparition)]
{
    // 當然你可以用任意合法的符號替換'a和'b,它們只是個名字
    let obj<'b>;                  //---'a start here
    {
        let x = Obj{};             //---'b start here

        obj<'b> = &'b x;           //---'b finish here
    }

    // obj借用是否有效,僅僅取決於它實際上它所借用的對象的生命週期作用域'b範圍是否大於等於'a
    // 一個'b作用域內的對象的借用,在'a內被調用,但是'b比'a小,調用的時候'b已經不存在了
    // 因此編譯器宣佈:這是非法的。
    println!("obj: {}", obj<'b>); //---'a finish here, 'b is out of scope, compile error!
}

二、結構體成員的 lifetime 標記:在 Rust 裏面一個結構體的成員變量如果是一個外部對象的借用,那麼必須標識這個借用對象的生命週期

struct Piece<'a>{
  slice:&'[u8] // 表明slice是來自外部對象的一個借用,'a只是一個生命週期形參
}


// Piece的定義裏面,'a 表示vec的生命週期,
// 下面的例子調用,vec的生命週期至少應該大於等於piece的生命週期
// 簡單說vec存活的作用域應該大於等於piece的存活作用域
fn test(){
  let vec = Vec::<u8>::new();
  let piece = Piece{slice: vec.as_slice()};
}

// 下面就是錯的, piece返回後,vec已經掛了
// 不滿足vec的生命週期大於等於piece的生命週期這條
fn test_2()->Piece{
  let vec = Vec::<u8>::new();
  let piece = Piece{slice: vec.as_slice()};
  piece // compile error: ^^^^^ returns a value referencing data owned by the current function
}

如果有兩個不同的成員,分別持有外部對象的借用,那麼他們應該使用一個生命週期標識還是兩個呢?

struct Piece<'a>{
  slice_1: &'[u8],  // 使用相同的生命週期標識
  slice_1: &'a [u8],  //
}

// Piece的定義裏面,'a只是表示slice_1和slice_2所借用的對象的存活範圍在一個相同的作用域內,
// 而不是說slice_1和slice_2所借用的對象必須是同一個,區分這點很重要
fn test_1(){
  // slice_1 和 slice_2 借用了同一個對象vec
  let vec = Vec::<u8>::new();
  let piece = Piece{slice_1: vec.as_slice(), slice_2: vec.as_slice()};
}

fn test_2(){
  // slice_1 和 slice_2 借用了兩個不同的對象
  let vec_1 = Vec::<u8>::new();
  let vec_2 = Vec::<u8>::new();
  let piece = Piece{slice_1: vec_1.as_slice(), slice_2: vec_2.as_slice()};
}

// 如果所借用的兩個對象的存活返回不同,'a只會取他們生命週期的最小的交集
// 下面這個例子,'a 和 vec_1的作用域相同
fn test_3(vec_2:&Vec<u8>){
  // slice_1 和 slice_2 借用了兩個不同的對象
  let vec_1 = Vec::<u8>::new();
  let piece = Piece{slice_1: vec_1.as_slice(), slice_2: vec_2.as_slice()};
}

// 因此,如果把piece返回就會出錯,因爲piece的生命週期不能超過vec_1
fn test_4(vec_2:&Vec<u8>)->Piece{
  // slice_1 和 slice_2 借用了兩個不同的對象
  let vec_1 = Vec::<u8>::new();
  let piece = Piece{slice_1: vec_1.as_slice(), slice_2: vec_2.as_slice()};
  piece
  // compile error: ^^^^^ returns a value referencing data owned by the current function
}

// 顯然,稍加改造就可以:
fn test_5<'a>(vec_1:&'a Vec<u8>, vec_2:&'a Vec<u8>)->Piece<'a>{
  // slice_1 和 slice_2 借用了兩個不同的對象
  let piece = Piece{slice_1: vec_1.as_slice(), slice_2: vec_2.as_slice()};
  piece
}

三、結構體成員函數的 lifetime 標記

結構體成員函數和普通函數一樣,可以有生命週期標識

struct Range{
    start: usize,
    len: usize
}

impl Range{
    // 接受一個外部的Vec對象的借用作爲參數
    // 返回這個Vec的片段的一個借用
    // 因此,需要引入生命週期標識
    // 表明返回的&[u8]的生命週期和傳入的owner的生命週期一致
    pub fn as_slice<'a>(&self, owner: &'a Vec<u8>)->&'[u8] {
        let slice = &owner[self.start..self.end()];
        slice
    }
}

下面的代碼會出錯:

enum AdvancedPiece{
    Range(Range),
    Vec(Vec<u8>)
}

impl AdvancedPiece{
    pub fn as_slice<'a>(&self, owner: & 'a Vec<u8>)->&'a [u8] {
        match self {
            AdvancedPiece::Range(range)=>{
                range.as_slice(owner) // range.as_slice(owner)返回的&[u8]生命週期和owner一致,用'a標記
            },
            AdvancedPiece::Vec(vec)={
                &vec // compile error: &vec的生命週期和owner並不一致
            }
        }
    }
}

結構體生命週期標識的一個需要注意的地方是,&self 也是可以標註生命週期的,因爲 & self 本身也是一個借用,既然是借用,就可以標記生命週期。從這個角度也可以進一步理解,生命週期就是標記借用對象的存活作用域用的。上述代碼,實際上等價於:

enum AdvancedPiece{
    Range(Range),
    Vec(Vec<u8>)
}

impl AdvancedPiece{
    // self有自己獨立的生命週期,用獨立的生命週期標識'b 標記出來
    // 這樣就看得更清楚了
    pub fn as_slice<'a,'b>(&'b self, owner: & 'a Vec<u8>)->&'[u8] {
        match self {
            AdvancedPiece::Range(range)=>{
                range.as_slice(owner) // range.as_slice(owner)返回的&[u8]生命週期和owner一致,用'a標記
            },
            AdvancedPiece::Vec(vec)=> {
                &vec // compile error: &vec的生命週期是'b , 返回值需要的是'a
            }
        }
    }
}

因此,我們可以標記 & self 和 owner 的生命週期是一致的來向編譯器說明需求:

enum AdvancedPiece{
    Range(Range),
    Vec(Vec<u8>)
}

impl AdvancedPiece{
    // 約定調用as_slice在self和owner的生命週期交集'a內是合法的
    pub fn as_slice<'a>(&'a self, owner: & 'a Vec<u8>)->&'a [u8] {
        match self {
            AdvancedPiece::Range(range)=>{
                range.as_slice(owner) // range.as_slice(owner)返回的&[u8]生命週期和owner一致,用'a標記
            },
            AdvancedPiece::Vec(vec)={
                &vec // 此時,&vec的生命週期也是'a
            }
        }
    }
}

四、省略生命週期標識 / 匿名生命週期標識

上述代碼裏面,Rust 在帶有生命週期標識的函數或者結構體調用的時候,允許省略顯式寫生命週期標識,就像泛型參數在編譯器可以自動推導類型時可以省略一樣:

fn args<T: ToCStr>(&mut self, args: &[T]) -> &mut Command                  // elided
fn args<'a, 'b, T: ToCStr>(&'a mut self, args: &'[T]) -> &'a mut Command // expanded

下面是結構體使用中省略生命週期標識的例子

struct Piece<'a>{
  slice:&'[u8] // 表明slice是來自外部對象的一個借用,'a只是一個生命週期形參
}

fn create_piece_1<'a>(vec:&'a Vec<u8>)->Piece<'a>{
    Piece{slice:&vec}
}

fn create_piece_2(vec:&Vec<u8>)->Piece{
    Piece{slice:&vec}
}

但是,有的時候,我們希望顯式表示生命週期,讓代碼更 “清晰”,可以用匿名生命週期

fn create_piece_3(vec:&Vec<u8>)->Piece<'_>{ // '_ 標記返回值Piece的生命週期參數,但是不必在函數和參數裏面標記生命週期
    Piece{slice:&vec}
}

同樣的,結構體的 impl 裏也可以用匿名生命週期簡化代碼:

impl<'a> Piece<'a>{
    fn create_piece_4(vec:&'a Vec<u8>)->Piece<'a>{
     Piece{slice:&vec}
    }
}

impl Piece<'_>{
    fn create_piece_5(vec:&Vec<u8>)->Piece<'_>{
     Piece{slice:&vec}
    }
}

五、結構體的一個成員變量借用另一個成員變量的情況

// TODO(先寫一個使用 Buffer/Pieces 的例子)

Rust 裏的 OO 編程

https://doc.rust-lang.org/book/ch17-01-what-is-oo.html

To many people, polymorphism is synonymous with inheritance. But it’s actually a more general concept that refers to code that can work with data of multiple types. For inheritance, those types are generally subclasses. Rust instead uses generics to abstract over different possible types and trait bounds to impose constraints on what those types must provide. This is sometimes called bounded parametric polymorphism.

其中,傳統 OO 裏多態是運行時多態,常規的實現是通過繼承來達成的:Inheritance as a Type System and as Code Sharing ,但是繼承共享代碼一般會導致三種問題:

從 C++ 的模版編程 + Concept 概念開始,泛型 + 萃取這種編譯期,通過兩種不同的抽象維度來實現多態,叫做:bounded parametric polymorphism.

Rust 在 OO 編程上的選擇,採用的正是完備的編譯期 OO + 多態設計:

Rust 靜態分發 (Static Dispatch)

Rust 基於 Trait 實現靜態分發,所謂靜態分發就是指在編譯期實現多態。

情景 1:

trait Echo{
  fn echo(&self);
}

struct Test{

}

struct Test2{

}

impl Echo for Test{
  fn echo(&self){

  }
}

impl Echo for Test2{
  fn echo(&self){

  }
}

fn do_something(t:&impl Echo){

}

fn get_something(value:bool)->impl Echo{
   if value {
     Test{}
   }else{
     Test2{}
   }
}

let t = Test{}
do_something(&t);
let v = get_someting(false); // 編譯錯誤

這裏的impl Echo只是一個簡寫,編譯器會確定 t 的具體類型,但是一次調用中類型是唯一確定的,並不能動態切換,因此do_something()可以正確被靜態確定 t 的類型,但是get_something()編譯會出錯,因爲->impl Echo並不是說可以返回【任意實現了 Echo 的類型】,而只是一個簡寫,函數體內必須返回同一種類型。如果需要【任意實現了 Echo 的類型】,應該做成泛型:

fn get_somethig<T:Echo>(value:bool)->T{ //不過使用的地方如果編譯器不能推導出T的類型,應該明確指定T的類型
   if value {
     Test{}
   }else{
     Test2{}
   }
}

大部分時候,靜態分發都是和泛型一起使用的:

fn test<T,U>(t:&T)-> where T:Echo+Clone+Debug, U:Echo+Display{

}

這裏的Echo+Clone+Debug 屬於【Intersect Type】也就是 T 需要同時實現這幾個 Trait,泛型和 Trait 的配合是 Rust 靜態分發的基本範式。

Rust 動態分發 (Dynamic Dispatch)

動態分發,就是和傳統 OOP 那樣,在運行期才能確定類型,編譯器在編譯期只能確定其 Trait 類型。但是由於只知道 Trait 信息,無法確定具體類型,就不能確定類型的確定性大小,因此不能在 Stack 上分配對象,需要用 Box 包一層,T 分配在 Heap 上。Box 指針則是確定性大小的,指針本身分配在 Stack 上。又爲了避免 Box 的含義的混淆,語法上需要加dyn關鍵字:Box,例如

fn test(t: Box<dyn Echo>){

}

參考:[1] https://blog.rust-lang.org/2015/05/11/traits.html

Rust 的閉包

一句話說明 Rust 的閉包:閉包的本質是編譯器幫你生成了一個實現 (impl) 了 Fn/FnMut/FnOnce 等 Trait 的匿名 struct

Rust 容器和函數式編程

Rust 的類型設計

Rust 模式匹配

枚舉類型配合模式匹配使用是最佳搭檔

enum Test{ A(i32), B(String) }
let t = Test::A(0);
match t {
  Test::A(v)=>{},
  Test::B(v)=>{}
}

Rust 錯誤處理

錯誤處理可以用if模式匹配:

fn test()->Result<T,Error>{}

let ret = test();
if let Err(e) = ret {

}
let value = ret.unwrap();

可以用直接模式匹配:

fn test()->Result<T,Error>{}

match test() {
  Ok(value)=>{},
  Err(e)=>{}
}

但是最常用用的是錯誤可選的錯誤類型映射 + 問號求值,錯誤處理不再卡殼主線流程:

fn test()->Result<String,Error>{

}

fn other()->Result<String, OtherError>{
   let value = test().map_err(|err|{
      // 錯誤類型轉換,同類型就不需要轉換
      Err(OtherError::from(err))
   })?;  // 問號求值,如果出錯就直接返回錯誤,規避了其他語言的各種if err 處理

   // do something...

   Ok(value)
}

Rust 多線程編程

Rust 異步編程

https://book.async.rs/introduction.html

在當前 Executor 裏發起一個異步任務

use async_std::task;
task::spawn(async move {

});

在一個線程裏發起異步任務

use async_std::thread;
thread::spawn(move ||{
  task::block_on(async { {

  });
})

示例的鏈式異步 + 錯誤處理 + 異步 + 錯誤處理...

let v = fetch().await.map_err(|err|{...})?.another_fetch().await.map_err(|err|{...})?;

本質上並不存在【真異步】,所有的異步都是僞裝出來的,本質上【異步 = 獨立開一個線程循環輪詢】

async/await提供了魔法,但是拆開盒子又沒有魔法,這是編程的核心樂趣所在。

如何寫一個定時器泵:

use async_std::prelude::*;
use async_std::stream;
use std::time::Duration;

let mut interval = stream::interval(Duration::from_secs(4));
while let Some(_) = interval.next().await {
    println!("prints every four seconds");
}

如何寫一個可調度的定時器泵:

// 創建一個channel
let (cmd_sender, cmd_recver) = async_std::sync::channel(8);

// 異步創建一個泵
async_std::task::spawn(async move {
    loop {
        let cmd_recver =  ctx.cmd_recver.clone();
        let cmd = async_std::io::timeout(Duration::from_millis(500), async move {
            cmd_recver.recv().await.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
        }).await;

        // 此時要麼過了500毫秒,要麼cmd_recver收到了一個cmd_sender投遞的信號
    }
});

// 在其他地方調度
cmd_sender.send(());

Rust 日誌組件

use log::*;
use simple_logger;
fn main(){
  simple_logger::SimpleLogger::new().with_level(LevelFilter::Debug).init().unwrap();
  info!("{}",1000);
  warn!("{}",1000);
  error!("{}",1000);
  debug!("{}",1000);
}

Rust 常用的設計模式

Builder 模式:

pub struct Object{
  name: String,
  id: Option<u32>,
  email: Option<String>
}

impl Object{
  pub fn new(name:String)->ObjectBuilder{
    ObjectBuilder::new(name)
  }
}

pub struct ObjectBuilder{
  name: String,
  id: Option<u32>,
  email: Option<String>
}

impl ObjectBuilder{
  pub fn new(name:String)->Self{
    // Builder的構造函數只傳入必須有的字段
    Self{
      name,
      id:None,
      email: None,
    }
  }

  pub fn id(mut self, id:u32>)->Self{
    // 設置可選字段,注意self的所有權進來又出去
    self.id = Some(id);
    self
  }

  pub fn email(mut self, email:String)->Self{
    // 設置可選字段,注意self的所有權進來又出去
    self.email = Some(email);
    self
  }

  pub fn build(self)->Object{
    // 構造Obejct,注意self的所有權進來,成員都被move給了Object,self所有權結束使用
    Object{
      name: self.name,
      id: self.id,
      email: self.email
    }
  }
}

// 使用
let obj = Object::new(String::from("fanfeilong")).id(13u32).email(String::from("fanfeilong@example.com")).build();

Split 模式:

pub struct Object{
  name: String,
  data: Vec<u8>
}

pub struct ObjectMore{
  name: String,
  id: Option<u32>,
  email: Option<String>
}

impl Object{
  // 消耗掉self的所有權,返回成員元組
  pub fn split(self)->(String, data){
    (self.name, self.data)
  }
}

// 消耗掉Object,將其成員Move給ObjectMore,同時對data做進一步的細化轉換,保持最小內存分配開銷
let obj = Object{..}
let (name, data) = obj.split();
let (id, email)  = decode(data);
let obj_more = ObjectMore{name, id, email);

消除 lifetime 傳染

例如有如下的帶 lifetime 的 trait

pub trait RawDecode<'de>: Sized {
    fn raw_decode(buf: &'de [u8]) -> BuckyResult<(Self, &'de [u8])>;
}

爲它擴展一個 trait 時,生命週期會傳染到上層,需要外層傳入 buf:

pub trait FileDecoder<'de>: Sized {
    fn decode_from_file(file: &Path, buf: &'de mut Vec<u8>) -> BuckyResult<(Self, usize)>;
}

impl<'de,D> FileDecoder<'de> for D
    where D: RawDecode<'de>,
{
    fn decode_from_file(file: &Path, buf: &'de mut Vec<u8>) -> BuckyResult<(Self, usize){
        match std::fs::File::open(file) {
            Ok(mut file) ={
                // let mut buf = Vec::<u8>::new();
                if let Err(e) = file.read_to_end(buf) {
                    return Err(BuckyError::from(e));
                }
                let len = buf.len();
                let (obj, buf) = D::raw_decode(buf.as_slice())?;
                let size = len - buf.len();
                Ok((obj, size))
            },
            Err(e) ={
                Err(BuckyError::from(e))
            },
        }
    }
}

可以通過在 where 字句中使用 for 表達式來阻斷生命週期傳染,因爲我們可以確定 buf 的生命週期在函數內時夠用的:

pub trait FileDecoder2: Sized {
    fn decode_from_file(file: &Path) -> BuckyResult<(Self, usize)>;
}

impl<D> FileDecoder2 for D
    where D:  for<'de> RawDecode<'de>,
{
    fn decode_from_file(file: &Path) -> BuckyResult<(Self, usize){
        match std::fs::File::open(file) {
            Ok(mut file) ={
                let mut buf = Vec::<u8>::new();
                if let Err(e) = file.read_to_end(&mut buf) {
                    return Err(BuckyError::from(e));
                }
                let len = buf.len();
                let (obj, buf) = D::raw_decode(buf.as_slice())?;
                let size = len - buf.len();
                Ok((obj, size))
            },
            Err(e) ={
                Err(BuckyError::from(e))
            },
        }
    }
}

使用 trait 的關聯類型替代泛型:

pub trait DescType{
  fn type()->u32;
}

pub trait Object{

  type Desc: DescType;

  fn type_info()->String{
   let type = Desc::type();
   type.to_string()
  }
}

pub struct RealDescType{

}

impl DescType for RealDescType{
  fn type()->{ 0u32 }
}

pub struct RealObject{

}

impl Object for RealObject{
  type Desc = RealDescType;
}

// 可以在泛型裏使用Object,以及Object關聯的Desc類型
pub struct ObjectDescript<O:Object>{
   instance: O,
   desc: O::Desc, // 則Desc可以跟隨O發生變化,這屬於編譯期多態
}

// 可以根據Desc是否實現了某些Trait來爲ObjectDescript自動實現某些Trait
// 例如,如果O::Desc實現了Debug,則自動爲ObjectDescript<O>實現Debug
impl Debug for ObjectDescript<O> where O: Object, O::Desc: Debug{

}

泛型組合的方式的代碼複用

泛型成員變量可以達到基於組合來做基類 / 子類的能力,子類變成了一個需要被組合的泛型類型參數,例如:

pub trait Sub{
  type Desc: ObjectDesc;
}
pub struct Base<Content:Sub>{
  name: String,
  desc: Content::Desc,  // 子類通過關聯類型來【定製】父類的某些關鍵成員變量的類型,但是該成員變量的佈局是放在父類這裏,子類Content本身不需要持有desc。
  content: Content      // 直接嵌入的子類部分數據
}

這裏的父類 / 子類,只是一個兼容傳統 OOP 的說法,實際上這裏都是泛型類。

消除循環依賴,規避所有權複雜度

如果 A 和 B 互相依賴

pub struct A{
    b: B
}

pub struct B {
    a: A
}

拆解出一個共同的部分 C 來消除依賴:

pub struct C {

}

// 讓A和B共同依賴C,A和B之間保持線性依賴
pub struct A {
  c: C,
  b: B
}

// 則A裏面需要被B調用的方法只要做成非成員方法即可:
impl A{
  pub fn call(c: &C){

  }
}

pub struct B {
  c: C
}

impl B {
  pub fn some(&self){
    // B根本不需要持有A,只要有C就可以調用A,或者call直接就是C的方法即可
    A::call(&self.c)
  }
}

事件系統

/// ## 定義一個訂閱回調Trait
#[async_trait]
pub trait FnSubscriber: Send + Sync + 'static {
    async fn call(&self, topic_id: TopicId, device_id:DeviceId) -> BuckyResult<()>;
}

/// ## 自動從Fn轉型爲FnSubscriber
#[async_trait]
impl<F, Fut> FnSubscriber for F
where
    F: Send + Sync + 'static + Fn(TopicId, DeviceId) -> Fut,
    Fut: Future<Output = BuckyResult<()>> + Send + 'static,
{
    async fn call(&self,  topic_id: TopicId, device_id:DeviceId) -> BuckyResult<(){
        let fut = (self)(topic_id, device_id); // 直接調用F:Fn(TopicId, DeviceId)
        let res = fut.await?; // 異步等待
        Ok(res.into())        // 返回結果
    }
}

pub struct Test{
  subscribers: Vec<Arc<dyn FnSubscriber>>, //動態分發
}

impl Test{
  pub fn new()->Self{
    Self{
      subscribers: Vec::new(),
    }
  }

  // 註冊事件
  pub fn on_subscribe(&mut self, callback: impl FnSubscriber){
        self.subscribers.push(Arc::new(callback));
  }

  // 觸發事件
  async fn emit_subscribe(&self, topic_id: &TopicId, device_id:&DeviceId)->BuckyResult<()>{
    for callback in self.subscribers.iter() {
      callback.call(topic_id.clone(), device_id.clone()).await?;
    }
    Ok(())
  }
}

異步編程中,Arc 和 Mutex 的正確用法

首先看下 Arc 和 Mutex 的正確配合:

  1. 需要多線程共享所有權的對象,一律用 Arc 即可

  2. Arc 導致 T 是隻讀的,但是你肯定需要修改某些成員變量

  3. 難道就直接 Arc 麼?每次使用的時候 obj.lock().unwrap().member = xxx?

  4. No!粒度太大,只應該在T的需要被修改的成員變量上加Mutex

  5. 如果那個成員變量也不是葉子節點,還有內部的結構,應該繼續【下推】到 T 的需要修改的成員變量上去添加 Mutex

例如:

// 頂層類型是個Arc<T>的封裝,使用new type的方式包裝一層
// Something可以被安全都在多線程task裏clone後傳遞
struct Something(Arc<Something>);
impl Something(Arc<Something>){
  new(y:String,a:String,p:u32)->Self{
    return Self{
      0:SomethingInner{y,x:Other{a,b:Third{p,q:Mutex::new(Vec::new())}}}
    }
  }

  // TODO:在此添加暴露SomethingInner方法給外部的成員函數,這個重複是必要的
}

struct SomethingInner{
  y: String;
  x: Other;
}

struct Other{
  a: String;
  b: Third;
}

struct Thrid{
  p: u32;
  q: Mutex<Vec<String>>; // 如果只有這個需要修改,只需這裏加Mutex
}

impl Third{
  fn append(&self, e:String){
    self.q.lock().unwrap().push(e); // 通過Mutex的內部可變性來修改q
  }
}

其次,我們看下同步鎖和異步鎖

  1. Rust 的同步庫裏面有同步的鎖:std::sync::Mutex

  2. Rust 的 async_std 裏有一個異步鎖:async_std::sync::Mutex

  3. 它們的區別是async_std::sync::Mutex實現了Send接口,因此可以跨越await點,例如:

async fn append(&self, e:String){
    // 獲取異步鎖的Guad對象
    let list = self.q.lock().await().unwrap(); // 通過Mutex的內部可變性來修改q

    // 異步調用點
    // 調度器會可能會在此處返回後下次再次進入到這裏繼續後面的執行,
    // 兩次執行可能不在一個線程
    waint().await();

    // 使用異步鎖的Guad對象
    // 這裏可能和list獲取時不在一個線程,因此,list需要實現`Send`
    // 同步鎖無此能力
    list.push(e);
}

但是,上述做法大部分時候時錯的。原因在於異步鎖改變了鎖的作用:

  1. 在同步鎖的時候,只是用同步鎖來【鎖定對變量的讀寫修改】這個最小粒度

  2. 在異步鎖的時候,鎖被用來鎖定了一堆異步行爲,這【擴大了鎖的粒度】,以及【延遲了鎖的釋放時機】

  3. 上述第 2 點導致了性能可能出現巨大劣化。

  4. 最重要的是這沒必要,大部分時候你只需在【對變量做原子修改時加同步鎖即可】

  5. 如果你需要【鎖定多個行爲】,此時你需要的不是鎖,而是在【使用同步鎖做入口控制】,類似 SQL 語句裏,使用表的主鍵在入口處做併發防護。

  6. 再往下,如果你需要保證一堆操作要麼實現,要麼都不實現,此時你需要的是【事務】。

  7. 簡單說,大部分時候,不要使用async_std::sync::Mutex

通過 Trait 來擴展 Trait

Rust 的孤兒原則導致,如果一個 struct S 和一個自定義 trait T 都不在該項目中,無法使用 T 爲 S 添加擴展,也無法爲 S 提供新的 impl。因此,可以通過定義一個新的在本項目裏的 trait,來爲某個不在本項目裏的 struct 實現擴展,也可以是爲實現了某個 Trait 的泛型提供擴展。

Rust 應用開發

Rust 工具鏈

Rust 社區

參考資料

[1] 給 Java/C#/C++ 程序員的 Rust 介紹,這種教程風格是我最喜歡的,通過 Diff,Step By Step 引入概念設計上的不同:https://fasterthanli.me/articles/i-am-a-java-csharp-c-or-cplusplus-dev-time-to-do-some-rust
[2] 作者學習 Rust 遇到的一個個怪 (阻撓,Frustrat),但是作者一步步打怪升級,把概念喫的很透徹:https://fasterthanli.me/articles/frustrated-its-not-you-its-rust
[3] Rust 小抄:https://www.programming-idioms.org/cheatsheet/Rust
[4] Rust 引入了一堆概念,可以看看王垠對 Rust 的設計上的一些問題的評價:http://www.yinwang.org/blog-cn/2016/09/18/rust
[5]why rust is meant to replace c https://evrone.com/rust-vs-c
[6] awesome-rust: rust 常用庫大全 https://github.com/rust-unofficial/awesome-rust
[7] 使用 vector-index 方式構造 graphs 數據結構 http://smallcultfollowing.com/babysteps/blog/2015/04/06/modeling-graphs-in-rust-using-vector-indices/
[8] 使用 rc > 方式構造 graphs 數據結構 https://github.com/nrc/r4cppp/blob/master/graphs/README.md
[9] Rust Async Book,看這個文檔就夠了:https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html

原文鏈接:https://www.cnblogs.com/math/p/rust.html 作者 範飛龍

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/IgtIr1W3IxHKZCsMx0EARg