现代编程语言:Rust (铁锈,一文掌握钢铁是怎样生锈的)

Posted 幻灰龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了现代编程语言:Rust (铁锈,一文掌握钢铁是怎样生锈的)相关的知识,希望对你有一定的参考价值。

五种我认为值得掌握的现代编程语言:

  • C(竞品:Zig): Unix/Linux/基础库 等一大波老牌开源基础库和平台开发
  • JavaScript(升级:TypeScript):浏览器/NodeJS后端/各种App内的Web开发,代表的是Web平台
  • Python(竞品:Julia):代表的是一大堆AI工具支持的脚本环境
  • Go:代表的是一部分的后端开发
  • Rust:代表的是替代了C++的大规模底层开发,Rust的开发能力覆盖了C++,但是又没有C++那一堆问题,拥有新的表达力和生命周期控制,并且它对Web平台是对接的。

我刻意剔除了三种大语言(仅在本文语境下讨论,不限实际需求考虑):

你认同么?我认同,并且我认为学校教了C语言之后,可以直接教Rust(TODO: 这里有一些支撑的理由,可以再讨论)。

我也直接剔除了各种函数式语言:

  • scheme
  • Haskell

函数式语言的的一些范式一直被融入到主流语言里面,日常开发也几乎用不到函数式语言,在函数式语言里面投入时间,边际收益并不高,但你可以花一个暑假沉浸进去认真感受一次,这样就够了。

Rust 开发环境配置

安装rustup

在线执行测试|playground

Rust Playground

安装VSCode插件

  • Rust support for Visual Studio Code
  • rust-analyzer
  • TOML Language Support
  • VS左侧搜索file to exclude可以配上**/lib*.json,,在查找的时候忽略Rust自己生成的配置文件

掌握Rust的命令行工具链

  • rustup: 一般用来安装/更新 rust的版本,切换stable和nightly版本用
  • rustc:rust的编译器,一般不需要手工调
  • cargo:一般通过cargo来管理rust的crate(rust的包叫做crate),同时rust的项目编译管理都用cargo,99%的情况下,你只会需要cargo命令即可。

Rust的工程结构

  • TOML
  • Cargo.toml

Rust的模块组织

上图是Rust典型项目文件系统和对应的模块系统,解释如下:

Rust项目根目录声明和导出模块

  • Rust项目,如果存在main.rs,项目可以被编译为bin可执行文件
  • Rust项目,如果存在lib.rs,项目可以被其他项目作为lib引用,其他项目在其Cargo.toml里的[dependencies]里指定my_project=path="../my_project"即可。
  • lib.rs和main.rs是可选的,可以同时存在或者只有其中一个
  • 在main.rs或者lib.rs文件内,通过如下的方式声明当前目录下存在的其他子模块
mod config;
mod manager;
mod objects;
mod util;
  • lib.rs里还可以指定导出哪些模块:
mod config;
mod manager;
mod objects; // 含有mod.rs的子目录是一个子模块
mod util;    // 含有mod.rs的子目录是一个子模块

pub use manager::*; // 指定全导出
pub use objects::AnyObject; // 指定导出objects模块内的AnyObject

项目子目录声明和导出模块

  • Rust含有mod.rs的子目录是一个子模块
  • 在子模块的mod.rs内,例如util模块,可以继续
    • 通过mod path_util;声明子模块
    • 通过pub use path_util::*;导出path_util模块内的所有可导出符号

使用其他模块

  • Rust项目根目录的顶级模块名为crate
  • Rust项目根目录下的一级模块是crate::xxxx,因此引用时应该写use crate::config::Config;
  • Rust的子目录下,例如path_util里引用本级cmd_util有两种方式
    • 如果cmd_util里的CmdUtil是pub的,并且有导出(例如util/mod.rs里pub use cmd_util::CmdUtil;),那么path_util里
      • 可以用use crate::util::CmdUtil从顶级模块crate开始指定路径
      • 也可以通过use super::CmdUtil指定。这是因为path_utilcmd_util在模块层级中的同级,可以通过super来表示上一级模块

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继承而来,对象生命周期的核心问题是:

  • 对象生命周期
    • Stack上对象的释放,一旦超出对象的作用域,就自动Destruct对象。
    • Heap上对象的释放,需要手动delete 来触发Destruct。
  • 对象状态管理
    • 在一个线程内,对象可被多处持有,单线程的多处持有点都可能修改对象的状态
    • 在多个线程内,对象可被多线程持有,多线程可并发地修改对象的状态

先看下对象的生命周期:

  • 单线程
    • Stack对象:
    • Heap对象
  • 多线程
    • Stack对象
    • Heap对象

再看下对象的状态管理:

  • 单线程
    • 不可变对象:可安全使用
    • 可变对象:对象状态需要被封装才能处于尽量可控
      • 例如把一个类的成员变量直接暴露出去,到处使用,就会带来封装泄漏,违反单一修改点原则
  • 多线程
    • 不可变对象:可安全使用
    • 可变对象:对象状态处于多线程共享时,需要有互斥机制,例如信号量和互斥锁
      • 并发修改对象,违反单一修改点原则
      • 并发修改对象,如果不加互斥,会带来对象的状态修改处于非原子修改状态,A线程修改了一半,B对修改了一半的脏数据进行读写。

Rust 所有权Ownership

Rust 引入了一个核心的语义:所有权(Owner),每个对象都有明确的所有权,所有权可以发生两种变化,下面是核心规则:

  • 移动(move),例如let x=String::from("test"); let y =x;,赋值语句let y=x;x的所有权移动给y,则x不再可用
    • 需要注意的是,并不是赋值语句都发生了所有权的移动
      • 内置类型(built in) 会执行按位拷贝,例如let x = 6; let y = x;
      • 实现了Copy这个trait的类型,会进行深拷贝
    • 可以看到在Rust里拷贝不是默认的,为了拷贝需要付出代价,这是根本性的设计和范式差异
      • 实现trait Copy,则赋值会自动逐bit拷贝
      • 实现trait Clone,则可以调用xx.clone()获得副本
  • 借用(borrow),将对象的所有权临时借给其他对象,借完要还的!借用又分成两种
    • 【1】不可变借用(immutable borrow):Rust允许一个变量同时有多个不可变借用,例如let x=String::from("test"); let y = &x; let z=&x;,则yz都是x的不可变借用
    • 【2】可变借用(mutable borrow):Rust只允许一个变量同时有一个可变借用,例如let x=vec![0;32]; let y=& mut x; let z=&mut x; y.push(0); 这里yz都发生了对x的可变借用,编译器会报错。
      • 请在单线程限定下思考这样设计解决了什么问题?

Rust 内部可变性(Internal mutability)

有时候,我们需要【不可变借用的内部成员变量可变,在Rust里面叫做内部可变性(Internal mutability)】。那么,有如下选择,它们内部都依赖底层的UnsafeCell实现,顾名思义这么做是unsafe的,但是编译器知道这些调用的地方需要特殊处理。

  • 单线程
    • 如果类型T实现了trait Copy,那么可以使用Cell<T>
    • 否则,可以使用RefCell<T>
  • 多线程
    • 使用互斥锁:Mutex<T>
    • 使用读写锁:RwLock<T>

Cell

对于实现了Copy的类型,可以使用 Cell<T>,官方例子:Cell in std::cell - Rust

  • 获取:如果T实现了Copy,则可以调用get方法,获得T的一份逐bit拷贝
  • 设置:使用set方法
  • 更新:使用update设置并返回新值
  • 替换:使用replace方法
  • 可变借用:使用get_mut方法获得Cell变量的可变借用,该方法继续遵循借用规则【1】【2】冲突原则。

改造下官方例子,官方例子里只改变了一次不可变借用的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>,常用方法主要是

  • 获得内部T的不可变借用:使用borrow()方法
  • 获得内部T的可变借用:使用borrow_mut()方法

虽然获得了对不可变借用内部成员的可变修改能力,但是借用的规则【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(Testx: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 跨线程传递/共享

对象在跨线程间使用

  • 【1】一个对象可以从线程A传递给线程B,此时需要对象类型实现 Send trait
  • 【2】一个对象的借用可以从线程A传递给线程B,此时需要对象类型实现 Sync Trait
    • 如果 T 实现了 Sync,则 & T 自动实现了Send => & T 可以从线程A传递给线程B

根据上面的规则【1】,实际上一个对象从线程A传递给线程B有如下情况

  • 原生指针即不实现 Send, 也不实现 Sync
  • Copy,既然都Copy了,每个线程持有一份独立拷贝
  • Move,既然Move了,每次只有一个线程有所有权,
  • 唯一所有权对象的Borrow
    • 不可变借用,多个线程间不可变借用,同时读取,遵循可同时多处不可变借用规则
    • 可变借用,一次只能有一个线程持有可变借用,唯一写
  • 多所有权对象的Borrow
    • Rc 即不实现 Send 也不实现 Sync,这是因为 Rc 的引用计数并没有使用Lock或者Aotomic,因此不能在多个线程间同时修改引用计数,不能在线程间 Send,更不能Sync了
    • Arc 实现了线程安全的引用计数,实现了Send,如果内部包含的类型可以Sync,则Arc<T> 也能 Sync
  • UnsafeCell 没有实现Sync,因此 CellRefCell 也没有实现 Sync,但是可以Send

Rust 内部共享(Internal sharing)

Rust的有所有权唯一原则,但是有些时候,我们需要在多处持有一个不可变对象的所有权,这叫做内部共享(Internal sharing)有两种情况

  • 单线程,此时,可以用Rc,这是一个引用计数指针
  • 多线程,此时,可以用Arc,这是一个多线程安全的引用计数指针,

组合使用

单线程:

  • 如果只需要唯一所有权
    • 遵循 Copy/Move/Borrow 规则
  • 如果需要多个所有权
    • 使用Rc
  • 无论是单所有权还是多所有权,如果需要只读对象的内部成员属性可修改
    • Copy类型使用 Cell
    • 否则使用RefCell

多线程:

  • 如果只需要维持唯一所有权
    • 只要T是Send的,就可以从线程A发送到线程B
    • 只要T是Sync的,就可以在线程A和线程B间,同时持有&T,但是因此只能由一个&mut T
  • 如果需要维持多所有权
    • 那么需要Arc,Arc实现了Send,如内部的T是Sync的,则Arc也是Sync的
      • 如果T是只读的就可以线程A发送到线程B
      • 也可以同时将 Arc的clone对象发送给多个线程,此时由于多个线程都持有所有权,因此自然是多个线程共享了内部的T
        • 如果T是只读的,那么Arc是Sync的,也就可以线程安全共享
        • 如果需要修改T,Arc 不是Sync的,因此必须用Arc<Mutext> 或者 Arc<RwLock> 制造只读对象的内部可变性
          • 如果只是T的某个成员变量需要写,那应该只要在那个成员变量上加Mutex即可,不必整个T都加Mutex

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:&'a [u8] // 表明slice是来自外部对象的一个借用,'a只是一个生命周期形参



// Piece的定义里面,'a 表示vec的生命周期,
// 下面的例子调用,vec的生命周期至少应该大于等于piece的生命周期
// 简单说vec存活的作用域应该大于等于piece的存活作用域
fn test()
  let vec = Vec::<u8>::new();
  let piece = Pieceslice: vec.as_slice(); 


// 下面就是错的, piece返回后,vec已经挂了
// 不满足vec的生命周期大于等于piece的生命周期这条
fn test_2()->Piece
  let vec = Vec::<u8>::new();
  let piece = Pieceslice: vec.as_slice(); 
  piece // compile error: ^^^^^ returns a value referencing data owned by the current function

如果有两个不同的成员,分别持有外部对象的借用,那么他们应该使用一个生命周期标识还是两个呢?

struct Piece<'a>
  slice_1: &'a [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 = Pieceslice_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 = Pieceslice_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 = Pieceslice_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 = Pieceslice_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 = Pieceslice_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>)->&'a [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>)->&'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的生命周期是'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: &'b [T]) -> &'a mut Command // expanded

下面是结构体使用中省略生命周期标识的例子

struct Piece<'a>
  slice:&'a [u8] // 表明slice是来自外部对象的一个借用,'a只是一个生命周期形参


fn create_piece_1<'a>(vec:&'a Vec<u8>)->Piece<'a> 
    Pieceslice:&vec


fn create_piece_2(vec:&Vec<u8>)->Piece 
    Pieceslice:&vec

但是,有的时候,我们希望显式表示生命周期,让代码更“清晰”,可以用匿名生命周期

fn create_piece_3(vec:&Vec<u8>)->Piece<'_> // '_ 标记返回值Piece的生命周期参数,但是不必在函数和参数里面标记生命周期 
    Pieceslice:&vec

同样的,结构体的impl里也可以用匿名生命周期简化代码:

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


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

五、结构体的一个成员变量借用另一个成员变量的情况

// TODO(先写一个使用Buffer/Pieces的例子)

Rust 里的OO编程

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.

  • 泛型( generic ):抽象不同类型
  • 萃取 ( trait ): 约束了泛型应该提供的能力

Rust在OO编程上的选择,采用的正是完备的编译期OO+多态设计:

  • 通过struct抽象数据,语法是 struct Data
  • 通过为struct 提供实现抽象行为,是否pub用来控制行为的封装,语法是impl Data fn method()
  • 通过是否公开数据和行为来控制封装细节,但是一般来说除非一个对象是用来做POD(Plain Old Data)的纯Component,一般数据结构的字段不应该暴露:
    • pub struct Data pub no: u32
    • impl Data pub fn new()->DataDatano:0
  • 通过泛型抽象不同类型:fn test<T>(t:T)
    • 可以通过在泛型上添加约束,表面这个泛型实现了哪些trait,例如:fn test<T> where T: Clone
  • 通过trait抽象类型必须拥有的能力:
    * trait Echo fn echo();
    * impl Echo for Data fn echo()
    * fn test<T>(t:T) where T:Echo
  • 这里的特点是,你可以为一个struct 提供不同的trait,例如:
    * trait Clone fn clone()->Self;
    * trait Echo fn echo()->Self;
    * impl Clone for Data fn copy()->Self...
    * impl Echo for Data fn echo()...
  • 这和传统OOP为一个class提供多个interface抽象并不相同
    * traitinterface 本身都是正交抽象的,一个抽象只做一件事
    * trait-struct 是通过外挂方式提供抽象,而interface-class 是通过继承方式提供抽象,这意味着当你不引入一个为某个struct提供的trait时,你看不到该struct的外挂,而interface则是耦合在class的实现里。
    * trait 是编译期多态,interface是运行期多态

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] Abstraction without overhead: traits in Rust | Rust Blog

Rust的闭包

一句话说明Rust的闭包:
闭包的本质是编译器帮你生成了一个实现(impl)了Fn/FnMut/FnOnce等Trait的匿名struct

Rust 容器和函数式编程

  • std::collections
    • Sequences: Vec, VecDeque, LinkedList
    • Maps: HashMap, BTreeMap
    • Sets: HashSet, BTreeSet
    • Misc: BinaryHeap
  • Rust Iterators
    • Iteration
    • Mapping
    • Filtering
    • Folding
    • Collecting
    • Composing
    • Some Real World Code

Rust 的类型设计

  • 原子类型
    • 整型:
      • u8/u16/u32/u64/u128
      • i8/i16/i32/i64/i128
      • usize
    • 布尔:
      • bool
  • struct XXX 结构体类型
  • enum C A(u32), B(String) 枚举类型 (带tag的Union),配合模式匹配使用
  • union 联合类型(C风格无tag的Union),Unsafe下配合模式匹配使用
  • tuple, (a,b,c)
  • unit 类型: (), 只有唯一的值()
  • new type: struct XXX();
    • 可以看成是【有名字的单元素tuple类型】,例如 struct MyString(String); 构造:let name = MyString(String::new("xxx")) 或者 let name = MyString0:String::new("xxx") 使用:println!(name.0)
    • new type 的目的是制造真正的新类型,如果使用 type MyString=String; 只是制造了一个别名。而使用new type则是制造了一个新的独立类型,代价是内部嵌套的类型的方法和属性都必须在新类型上重新导出才可以直接被外部使用,否则就得通过xxx.0先获取内部类型再调用。很多时候 new type可以解决封装问题和孤儿原则问题(TODO:如有必要此处可详细展开)。
  • trait: TypeClass
  • 字符串:
    • String, 堆上分配内存
    • &str,String的Slice类型
  • 容器
    • Vec<T>,堆上分配内存
    • &[T], Vec的Slice类型
  • 指针
    • 借用:& T, &mut T
    • 内部可变性:Cell<T>, RefCell<T>, Mutex<T>, RwLock<T>
    • 引用计数:Rc<T>, Arc<T>
    • 装箱:Box<T>, Pin<T>
  • 底类型(nerver): !

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 多线程编程

  • 锁,锁是一种制造多线程安全的内部可变性的指针
    • Mutex/RwLock
    • 有同步版本和异步版本
  • 引用计数,引用计数是一种制造多所有权的指针
    • 单线程用Rc
    • 多线程用Arc

Rust 异步编程

Introduction - Async programming in Rust with async-std

在当前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|...)?;

本质上并不存在【真异步】,所有的异步都是伪装出来的,本质上【异步=独立开一个线程循环轮询】

  • 独立开一个线程,循环轮询操作系统相关的事件,例如socket,这种轮询方式被叫做Reactor模式,每次轮询的时候问下系统是否有新的可用事件(Event)
  • 独立开一个线程,循环轮询一个Future,这种轮询是Executor做的。
    • 所谓Future就是提供了一个poll方法的对象,每次轮询的时候调用一次poll, 如果状态位Ready,就结束从队列里移除该Future,否则继续。
    • 而poll的实现里面,如果不返回Ready,就需要返回Pending同时持有下传递进来的一个waker对象,在数据准备好的时候调用下waker.wake(),通知Executor可以再次轮询。
    • 如果poll实现里刚好是一个和socket相关的操作,就要做下wake和socket相关的Event之间的一个映射,这样Reactor里的轮询到event的时候,就会找到影视的waker,调用wake,从而通知到Executor再次轮询。
    • Future是可组合的,await是组合Future的语法糖。一直组合到main函数,返回一个顶层的Future,被反复轮询。

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 = ObjectMorename, 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<Mutex>么?每次使用的时候 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:SomethingInnery,x:Othera,b:Thirdp,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 项目