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

06-10 01:36